diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b3092718d6c..aa0b6a2f64b 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -93,6 +93,9 @@ bundle_config.pb.json @BenHenning
LICENSE @BenHenning
NOTICE @BenHenning
+# Language configuration files.
+config/**/languages/*.textproto @BenHenning
+
#####################################################################################
# app module #
#####################################################################################
@@ -143,6 +146,9 @@ NOTICE @BenHenning
# Global domain module code ownership.
/domain/**/*.kt @BenHenning
+# Domain test resources.
+/domain/src/test/res/values/strings.xml @BenHenning
+
# Questions support.
/domain/src/*/java/org/oppia/android/domain/question/ @vinitamurthi
diff --git a/app/BUILD.bazel b/app/BUILD.bazel
index 0589cd75b45..e3c4910f7ed 100644
--- a/app/BUILD.bazel
+++ b/app/BUILD.bazel
@@ -577,7 +577,7 @@ kt_android_library(
"//third_party:androidx_databinding_databinding-runtime",
"//third_party:circularimageview_circular_image_view",
"//utility/src/main/java/org/oppia/android/util/accessibility",
- "//utility/src/main/java/org/oppia/android/util/caching:caching_module",
+ "//utility/src/main/java/org/oppia/android/util/caching:caching_prod_module",
"//utility/src/main/java/org/oppia/android/util/logging:prod_module",
"//utility/src/main/java/org/oppia/android/util/logging/firebase:prod_module",
],
@@ -690,6 +690,7 @@ kt_android_library(
"//third_party:javax_annotation_javax_annotation-api_jar",
"//utility",
"//utility/src/main/java/org/oppia/android/util/accessibility:prod_module",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
"//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions",
# TODO(#2432): Replace debug_module with prod_module when building the app in prod mode.
"//utility/src/main/java/org/oppia/android/util/networking:debug_module",
@@ -773,6 +774,7 @@ TEST_DEPS = [
"//utility",
"//utility/src/main/java/org/oppia/android/util/accessibility",
"//utility/src/main/java/org/oppia/android/util/accessibility:test_module",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
"//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module",
]
diff --git a/app/build.gradle b/app/build.gradle
index daa5e412ae5..048c6160c0e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -120,6 +120,7 @@ dependencies {
'com.google.firebase:firebase-core:17.5.0',
'com.google.firebase:firebase-crashlytics:17.0.0',
'com.google.guava:guava:28.1-android',
+ 'com.google.protobuf:protobuf-javalite:3.17.3',
'com.github.oppia:CircularImageview:35d08ba88a',
'de.hdodenhof:circleimageview:3.0.1',
'nl.dionsegijn:konfetti:1.2.5',
diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt
index dfc1c2cabba..93c8f28f2fa 100644
--- a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt
+++ b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt
@@ -37,6 +37,7 @@ import org.oppia.android.domain.question.QuestionModule
import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
import org.oppia.android.util.accessibility.AccessibilityProdModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CachingModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -87,7 +88,7 @@ import javax.inject.Singleton
PlatformParameterModule::class, ExplorationStorageModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class,
- NetworkConfigProdModule::class,
+ NetworkConfigProdModule::class, AssetModule::class,
// TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then
// directly exclude debug files from the build and thus won't be requiring this module.
NetworkConnectionDebugUtilModule::class
diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt
index 75e6862ce9d..fceda133cb3 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt
@@ -95,6 +95,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -710,7 +711,8 @@ class AdministratorControlsActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt
index c06a54996b2..d2586105055 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt
@@ -74,6 +74,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -273,7 +274,8 @@ class AppVersionActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt
index c86305b7537..2c17d492321 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt
@@ -79,6 +79,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -493,7 +494,8 @@ class CompletedStoryListActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt
index 17326d84554..d20a1351c23 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt
@@ -58,6 +58,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -151,7 +152,8 @@ class LessonThumbnailImageViewTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt
index 214eb1e6093..174e85a9143 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt
@@ -68,6 +68,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -289,7 +290,8 @@ class MarginBindingAdaptersTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
/** Create a TestApplicationComponent. */
diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt
index 3ca29443376..137c478ea85 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt
@@ -70,6 +70,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -477,7 +478,8 @@ class StateAssemblerMarginBindingAdaptersTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
/** Create a TestApplicationComponent. */
diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt
index bbe14ac57dd..b9d73477a13 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt
@@ -68,6 +68,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -475,7 +476,8 @@ class StateAssemblerPaddingBindingAdaptersTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt
index 68e55b7d9da..318dccedf78 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt
@@ -65,6 +65,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -209,7 +210,8 @@ class ViewBindingAdaptersTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
/** Create a TestApplicationComponent. */
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt
index 8e182ef7a03..e50b5e9faa6 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt
@@ -78,6 +78,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -276,7 +277,8 @@ class DeveloperOptionsActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt
index adc149d799d..295a29be211 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt
@@ -80,6 +80,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -604,7 +605,8 @@ class DeveloperOptionsFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt
index c3c50bc8e35..ffbc7f7bac8 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt
@@ -60,6 +60,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -166,7 +167,8 @@ class MarkChaptersCompletedActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt
index 66bf6e135f4..4eb5895a733 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt
@@ -72,6 +72,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -870,7 +871,8 @@ class MarkChaptersCompletedFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt
index b0c105b6452..fce67ba888f 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt
@@ -60,6 +60,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -166,7 +167,8 @@ class MarkStoriesCompletedActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt
index b2fb88bd770..7dd9f01c1ee 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt
@@ -72,6 +72,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -571,7 +572,8 @@ class MarkStoriesCompletedFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt
index cb4f7812c9b..4e3cc82a187 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt
@@ -60,6 +60,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -166,7 +167,8 @@ class MarkTopicsCompletedActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt
index 9991d6a82fe..aafa86d1505 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt
@@ -72,6 +72,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -541,7 +542,8 @@ class MarkTopicsCompletedFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt
index c6fc44819e4..936ced5f422 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt
@@ -60,6 +60,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -154,7 +155,8 @@ class ViewEventLogsActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt
index 32cd2dc56bd..6dd16ffdde9 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt
@@ -67,6 +67,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -609,7 +610,8 @@ class ViewEventLogsFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt
index 05d194a07e5..04e4425d862 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt
@@ -61,6 +61,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -158,7 +159,8 @@ class ForceNetworkTypeActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
/** [ApplicationComponent] for [ForceNetworkTypeActivityTest]. */
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt
index 9de95902887..d0ec55f1357 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt
@@ -67,6 +67,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -374,7 +375,8 @@ class ForceNetworkTypeFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
/** [ApplicationComponent] for [ForceNetworkTypeFragmentTest]. */
diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt
index 29e09b5ff14..482dc4adea8 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt
@@ -72,6 +72,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -226,7 +227,8 @@ class FAQListFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt
index e8dc5ca6bc7..025739b9038 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt
@@ -66,6 +66,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.DefaultResourceBucketName
import org.oppia.android.util.gcsresource.GcsResourceModule
@@ -198,7 +199,8 @@ class FAQSingleActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt
index 3a524ae6aff..2ae447c843d 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt
@@ -55,6 +55,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -131,7 +132,8 @@ class FaqListActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt
index ef127cd6d9e..440437555d2 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -133,7 +134,8 @@ class HelpActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
NetworkModule::class, ExplorationStorageModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt
index 4e1e30d0a31..e4cdcc5e5f5 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt
@@ -83,6 +83,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1171,7 +1172,8 @@ class HelpFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt
index 9eb62ea282d..e93908bf655 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt
@@ -98,6 +98,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1467,7 +1468,8 @@ class HomeActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt
index 20b7c233b7e..5f0463580e2 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt
@@ -99,6 +99,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1485,7 +1486,8 @@ class RecentlyPlayedFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt
index 38c8d7576fd..74b33b75346 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt
@@ -56,6 +56,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -343,6 +344,7 @@ class TopicSummaryViewModelTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt
index 700bca38d21..7c314643011 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt
@@ -57,6 +57,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -338,6 +339,7 @@ class WelcomeViewModelTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt
index d8c95d63de4..b2090c6cf2e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt
@@ -57,6 +57,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -347,6 +348,7 @@ class PromotedStoryListViewModelTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt
index 44d20679d40..253599fd6fc 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt
@@ -55,6 +55,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -358,6 +359,7 @@ class PromotedStoryViewModelTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt
index c1add50efe4..9149dfd06cb 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt
@@ -64,6 +64,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -212,7 +213,8 @@ class MyDownloadsFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt
index de17ca73aaa..f330650d729 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -130,7 +131,8 @@ class OnboardingActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt
index 3d7de3f3d88..64853a4919f 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt
@@ -82,6 +82,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -673,7 +674,8 @@ class OnboardingFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt
index 663ab1c47a2..496c911f13d 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt
@@ -77,6 +77,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -437,7 +438,8 @@ class OngoingTopicListActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt
index dac16b30ccc..fc439789955 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -140,7 +141,8 @@ class AppLanguageActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt
index c24add0959b..717349284b1 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt
@@ -65,6 +65,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
@@ -235,6 +236,7 @@ class AppLanguageFragmentTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt
index d87fcf61a6e..6505c0cc6e8 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -140,7 +141,8 @@ class AudioLanguageActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt
index f14f82a0ee9..80a126fe483 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt
@@ -64,6 +64,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
@@ -228,6 +229,7 @@ class AudioLanguageFragmentTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt
index 90f9adb4b4e..5b4a99e556e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -132,7 +133,8 @@ class OptionsActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt
index 239a13655ce..4f6073a5b6f 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt
@@ -77,6 +77,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.EnableConsoleLog
@@ -631,7 +632,8 @@ class OptionsFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt
index d8218dd3716..395605162ec 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -140,7 +141,8 @@ class ReadingTextSizeActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt
index 7201f023edd..175079f887d 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt
@@ -71,6 +71,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
@@ -294,6 +295,7 @@ class ReadingTextSizeFragmentTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt
index f6f037f0867..134aa6ec9ab 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -232,6 +233,7 @@ class CustomBulletSpanTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt
index 3c21325350b..12dfb0dcb17 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt
@@ -83,6 +83,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.DefaultResourceBucketName
import org.oppia.android.util.gcsresource.GcsResourceModule
@@ -634,7 +635,8 @@ class HtmlParserTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt
index 79e2fcc9427..ef8b3107676 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt
@@ -85,6 +85,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -460,7 +461,8 @@ class AudioFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt
index 7627605d290..9c20029e9c5 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt
@@ -116,6 +116,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1738,7 +1739,8 @@ class ExplorationActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
TestExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt
index 6db54dbd925..8d9dd8dd6db 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt
@@ -142,6 +142,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadImagesFromAssets
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
@@ -2036,7 +2037,8 @@ class StateFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class,
- NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class
+ NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt
index d7e480a090b..908a5cc9432 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt
@@ -95,6 +95,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1633,7 +1634,8 @@ class AddProfileActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt
index 47f0fbfe30d..f65716eb7b7 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt
@@ -82,6 +82,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -644,7 +645,8 @@ class AdminAuthActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt
index bb1c8a4938e..92c0c6fa7e5 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt
@@ -91,6 +91,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1095,7 +1096,8 @@ class AdminPinActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt
index 450bdaffb1d..3b95f2c7096 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt
@@ -85,6 +85,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1102,7 +1103,8 @@ class PinPasswordActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt
index 033cca4f60b..1a8726055ea 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt
@@ -79,6 +79,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
import org.oppia.android.util.gcsresource.GcsResourceModule
@@ -509,7 +510,8 @@ class ProfileChooserFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt
index 7d91c69f017..ab5bde34c2e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt
@@ -60,6 +60,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -144,7 +145,8 @@ class ProfilePictureActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt
index e45d3a9dff1..4be8dfc07d9 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -134,7 +135,8 @@ class ProfileProgressActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt
index 9423bb9d83e..6458c787b01 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt
@@ -96,6 +96,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.EnableConsoleLog
@@ -739,7 +740,8 @@ class ProfileProgressFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt
index a444ae25af2..42fe9c46f5e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt
@@ -89,6 +89,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -738,7 +739,8 @@ class BindableAdapterTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt
index ef1068cdd0e..d139b7c42ec 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt
@@ -74,6 +74,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -205,7 +206,8 @@ class ResumeLessonActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt
index 11cb228841f..a4fd3694488 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt
@@ -68,6 +68,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -235,7 +236,8 @@ class ResumeLessonFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt
index 6b867a85225..632e20c858a 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt
@@ -77,6 +77,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
import org.oppia.android.util.gcsresource.GcsResourceModule
@@ -556,7 +557,8 @@ class ProfileEditActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt
index e6c00278681..a4cf80f8c2f 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -130,7 +131,8 @@ class ProfileListActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt
index 90fef1c9528..62d6701ab8c 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt
@@ -71,6 +71,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -368,7 +369,8 @@ class ProfileListFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt
index d0ce6d1b1b5..95e737e1ba4 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt
@@ -81,6 +81,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -468,7 +469,8 @@ class ProfileRenameActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt
index fd3cb754575..56bcae2daf9 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt
@@ -83,6 +83,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1023,7 +1024,8 @@ class ProfileResetPinActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt
index b256d050eb5..d2603a6cc7e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt
@@ -71,6 +71,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -306,7 +307,8 @@ class SplashActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt
index d029d592d70..ae24740633d 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt
@@ -74,6 +74,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -214,7 +215,8 @@ class StoryActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt
index 5ab17a39e18..dbe592d7211 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt
@@ -110,6 +110,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -730,7 +731,8 @@ class StoryFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt
index fba2d4c96b6..f498e07e9f6 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt
@@ -63,6 +63,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -203,7 +204,8 @@ class DragDropTestActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt
index f794d929aca..ff3d821b16c 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt
@@ -74,6 +74,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -301,7 +302,8 @@ class ImageRegionSelectionInteractionViewTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt
index 8adaf149c09..ad919277ebd 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt
@@ -70,6 +70,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1019,7 +1020,8 @@ class InputInteractionViewTestActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt
index d893d0dc5ce..d4ee9bf4e43 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt
@@ -96,6 +96,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -423,7 +424,8 @@ class NavigationDrawerActivityDebugTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt
index fa3f20f5e12..e9cb4cbbb9d 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt
@@ -106,6 +106,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -972,6 +973,7 @@ class NavigationDrawerActivityProdTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt
index eb240b41212..8901f0b4cf6 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt
@@ -58,6 +58,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -180,7 +181,8 @@ class TestFontScaleConfigurationUtilActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class, AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt
index ade9ca0d589..5f2ffd565b3 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt
@@ -65,6 +65,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -178,7 +179,8 @@ class TopicTestActivityForStoryTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt
index 7f3fe4447bf..601bb6ac9b8 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt
@@ -55,6 +55,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -142,7 +143,8 @@ class LicenseListActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt
index 1a8165d6d9e..c0304c4fb52 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt
@@ -73,6 +73,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -356,7 +357,8 @@ class LicenseListFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt
index 258955fe487..41813853cd3 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt
@@ -56,6 +56,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -151,7 +152,8 @@ class LicenseTextViewerActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt
index f1087caf478..9d6d6bfb5f9 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt
@@ -63,6 +63,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -334,7 +335,8 @@ class LicenseTextViewerFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt
index 61b55264b26..7bc6b28c583 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt
@@ -55,6 +55,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -139,7 +140,8 @@ class ThirdPartyDependencyListActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt
index 4ffff7e8433..9e8ed1db6e4 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt
@@ -72,6 +72,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -466,7 +467,8 @@ class ThirdPartyDependencyListFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt
index 432391a0c69..c615f95f266 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -140,7 +141,8 @@ class TopicActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt
index 7d961425991..7e16d115b8e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt
@@ -77,6 +77,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -601,7 +602,8 @@ class TopicFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt
index 3f9fdb6b2b3..9951624ae50 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt
@@ -74,6 +74,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadImagesFromAssets
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
@@ -303,7 +304,8 @@ class ConceptCardFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt
index 450119542e6..10e10daa277 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt
@@ -83,6 +83,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -426,7 +427,8 @@ class TopicInfoFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt
index 5b445f44c83..855cba5c4ee 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt
@@ -97,6 +97,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -914,7 +915,8 @@ class TopicLessonsFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt
index 3abbf827b28..74a1acc5288 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt
@@ -79,6 +79,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -414,7 +415,8 @@ class TopicPracticeFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt
index 5ac47e75e97..d0a086b5af2 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt
@@ -106,6 +106,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -527,7 +528,8 @@ class QuestionPlayerActivityTest {
LogUploadWorkerModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt
index 1c5c516429d..f5bab908d76 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt
@@ -82,6 +82,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -310,7 +311,8 @@ class TopicRevisionFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt
index 902fe961533..043e58229b0 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt
@@ -55,6 +55,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -152,7 +153,8 @@ class RevisionCardActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
index 3c921de1c5b..c8d4b56fe52 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
@@ -87,6 +87,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadImagesFromAssets
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
@@ -436,7 +437,8 @@ class RevisionCardFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt
index e5f430db6c9..ffdc9a9b2e7 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt
@@ -50,6 +50,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -126,7 +127,8 @@ class RatioExtensionsTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt
index 1ae2a560a28..574dba12457 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt
@@ -60,6 +60,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -185,7 +186,8 @@ class WalkthroughActivityTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt
index ff4b24dbd46..bce1654de42 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt
@@ -68,6 +68,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -270,7 +271,8 @@ class WalkthroughFinalFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt
index d13a7f4625b..810ffeeee49 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt
@@ -72,6 +72,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadImagesFromAssets
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
@@ -296,7 +297,8 @@ class WalkthroughTopicListFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt
index 9dfb5fa1803..d1576a0f8e2 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt
@@ -65,6 +65,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -193,7 +194,8 @@ class WalkthroughWelcomeFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt
index cdfa64fc9db..b7ed266a2a0 100644
--- a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt
@@ -56,6 +56,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -134,6 +135,7 @@ class HomeActivityLocalTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt
index 261484739e8..147aba92b49 100644
--- a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt
+++ b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt
@@ -51,6 +51,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -445,7 +446,8 @@ class StringToFractionParserTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt
index e138607e594..951470b0e53 100644
--- a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt
+++ b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt
@@ -51,6 +51,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -221,7 +222,8 @@ class StringToRatioParserTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt
index bc2bde4595b..abf844bd674 100644
--- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt
@@ -62,6 +62,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -202,6 +203,7 @@ class ExplorationActivityLocalTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt
index 046a962960e..de4fde506bb 100644
--- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt
@@ -108,6 +108,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -1666,7 +1667,8 @@ class StateFragmentLocalTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt
index 9dcc85cfe2c..80c515a44dd 100644
--- a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -124,7 +125,8 @@ class ProfileChooserFragmentLocalTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt
index 9d8f5cf7fc4..c727a6a597a 100644
--- a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt
@@ -55,6 +55,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -148,7 +149,8 @@ class StoryActivityLocalTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt
index ec6236475ba..ef34ca446c1 100644
--- a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt
@@ -56,6 +56,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -156,6 +157,7 @@ class CompletedStoryListSpanTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt
index 0296ed9d216..887c5a4d22e 100644
--- a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt
@@ -56,6 +56,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -170,6 +171,7 @@ class HomeSpanTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt
index efa76a1a516..ed6742cba04 100644
--- a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt
@@ -57,6 +57,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -167,6 +168,7 @@ class OngoingTopicListSpanTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt
index a45763b659d..69509534265 100644
--- a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt
@@ -75,6 +75,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -335,7 +336,7 @@ class PlatformParameterIntegrationTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, TestNetworkModule::class, RetrofitTestModule::class,
NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class,
- NetworkConnectionDebugUtilModule::class
+ NetworkConnectionDebugUtilModule::class, AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt
index eaf8ceafeb4..cccc242142f 100644
--- a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt
@@ -56,6 +56,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -369,6 +370,7 @@ class ProfileChooserSpanTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt
index ca0fdc0cc11..2c1fb5d5334 100644
--- a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt
@@ -56,6 +56,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -152,6 +153,7 @@ class ProfileProgressSpanCount {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt
index 55774636509..c918092cc3c 100644
--- a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt
@@ -61,6 +61,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -288,6 +289,7 @@ class RecentlyPlayedSpanTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt
index af72cb962f2..5c8f51823b9 100644
--- a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt
@@ -55,6 +55,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -153,6 +154,7 @@ class TopicRevisionSpanTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt
index dcf522c7bcb..ea12303b4ac 100644
--- a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt
@@ -67,6 +67,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -213,7 +214,8 @@ class AdministratorControlsFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt
index 163acfa0737..f122ab908a9 100644
--- a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt
@@ -60,6 +60,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -210,7 +211,8 @@ class OptionsFragmentTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt
index 61ca0b0e135..3fa567d8737 100644
--- a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt
@@ -53,6 +53,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -181,6 +182,7 @@ class PlayerSplitScreenTesting {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt
index 1ee48a8f9fa..deefef26636 100644
--- a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt
+++ b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt
@@ -66,6 +66,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.accessibility.FakeAccessibilityChecker
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -195,6 +196,7 @@ class StateFragmentAccessibilityTest {
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class,
NetworkConfigProdModule::class
]
)
diff --git a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt
index 8b8f0917f09..86ae714076c 100644
--- a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt
@@ -53,6 +53,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -136,7 +137,8 @@ class TopicInfoFragmentLocalTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt
index 867884ecbf6..236bd0c33e8 100644
--- a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt
@@ -52,6 +52,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -138,7 +139,8 @@ class TopicLessonsFragmentLocalTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt
index b01dca3767f..5754c0ad09e 100644
--- a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt
@@ -82,6 +82,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -401,7 +402,8 @@ class QuestionPlayerActivityLocalTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt
index 12c49e48629..fe6fa8ca345 100644
--- a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt
@@ -54,6 +54,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.LoggerModule
@@ -128,7 +129,8 @@ class RevisionCardActivityLocalTest {
FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt
index b5dc50a600f..a0c85976ea1 100644
--- a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt
+++ b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt
@@ -51,6 +51,7 @@ import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.logging.EnableConsoleLog
@@ -148,7 +149,7 @@ class DateTimeUtilTest {
HintsAndSolutionConfigModule::class, ExpirationMetaDataRetrieverModule::class,
GlideImageLoaderModule::class, PrimeTopicAssetsControllerModule::class,
HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class,
- DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class
+ DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/build.gradle b/build.gradle
index 9730d7d0eb6..e0b2d5f2b98 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.1.1'
// NOTE: Do not place your application dependencies here; they belong
diff --git a/config/config_proto_assets.bzl b/config/config_proto_assets.bzl
new file mode 100644
index 00000000000..f3073c33163
--- /dev/null
+++ b/config/config_proto_assets.bzl
@@ -0,0 +1,30 @@
+"""
+Macro for generating proto assets for app-wide configurations.
+"""
+
+load("//model:text_proto_assets.bzl", "generate_proto_binary_assets")
+
+def generate_supported_languages_configuration_from_text_proto(
+ name,
+ supported_language_text_proto_file_name):
+ """
+ Converts multiple lists of text proto assets to binary.
+
+ Args:
+ name: str. The name of this generation instance. This will be a prefix for derived targets.
+ supported_language_text_proto_file_name: target. The target corresponding to the text proto
+ defining the list of supported languages in the app.
+
+ Returns:
+ list of str. The list of new proto binary asset files that were generated.
+ """
+ return generate_proto_binary_assets(
+ name = name,
+ names = [supported_language_text_proto_file_name],
+ proto_dep_name = "languages",
+ proto_type_name = "SupportedLanguages",
+ name_prefix = name,
+ asset_dir = "languages",
+ proto_dep_bazel_target_prefix = "//model",
+ proto_package = "model",
+ )
diff --git a/config/src/java/org/oppia/android/config/AndroidManifest.xml b/config/src/java/org/oppia/android/config/AndroidManifest.xml
new file mode 100644
index 00000000000..54ecb1d7e7e
--- /dev/null
+++ b/config/src/java/org/oppia/android/config/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/config/src/java/org/oppia/android/config/BUILD.bazel b/config/src/java/org/oppia/android/config/BUILD.bazel
new file mode 100644
index 00000000000..36079416378
--- /dev/null
+++ b/config/src/java/org/oppia/android/config/BUILD.bazel
@@ -0,0 +1,38 @@
+# TODO(#1532): Rename file to 'BUILD' post-Gradle.
+"""
+This package contains configuration libraries for defining & tweaking app-wide behavior.
+"""
+
+load("//model:text_proto_assets.bzl", "generate_proto_binary_assets")
+
+_SUPPORTED_LANGUAGES_CONFIG_ASSETS = generate_proto_binary_assets(
+ name = "supported_languages_config_assets",
+ asset_dir = "languages",
+ name_prefix = "supported_languages_config_assets",
+ names = ["supported_languages"],
+ proto_dep_bazel_target_prefix = "//model",
+ proto_dep_name = "languages",
+ proto_package = "model",
+ proto_type_name = "SupportedLanguages",
+)
+
+_SUPPORTED_REGIONS_CONFIG_ASSETS = generate_proto_binary_assets(
+ name = "supported_regions_config_assets",
+ asset_dir = "languages",
+ name_prefix = "supported_regions_config_assets",
+ names = ["supported_regions"],
+ proto_dep_bazel_target_prefix = "//model",
+ proto_dep_name = "languages",
+ proto_package = "model",
+ proto_type_name = "SupportedRegions",
+)
+
+android_library(
+ name = "languages_config",
+ assets = _SUPPORTED_LANGUAGES_CONFIG_ASSETS + _SUPPORTED_REGIONS_CONFIG_ASSETS,
+ assets_dir = "languages/",
+ manifest = "AndroidManifest.xml",
+ visibility = [
+ "//domain/src/main/java/org/oppia/android/domain/locale:__pkg__",
+ ],
+)
diff --git a/config/src/java/org/oppia/android/config/languages/supported_languages.textproto b/config/src/java/org/oppia/android/config/languages/supported_languages.textproto
new file mode 100644
index 00000000000..4e6d3f35138
--- /dev/null
+++ b/config/src/java/org/oppia/android/config/languages/supported_languages.textproto
@@ -0,0 +1,109 @@
+language_definitions {
+ language: ARABIC
+ min_android_sdk_version: 1
+ app_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "ar"
+ }
+ android_resources_language_id {
+ language_code: "ar"
+ }
+ }
+ content_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "ar"
+ }
+ }
+ audio_translation_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "ar"
+ }
+ }
+}
+language_definitions {
+ language: ENGLISH
+ min_android_sdk_version: 1
+ app_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "en"
+ }
+ android_resources_language_id {
+ language_code: "en"
+ }
+ }
+ content_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "en"
+ }
+ }
+ audio_translation_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "en"
+ }
+ }
+}
+language_definitions {
+ language: HINDI
+ min_android_sdk_version: 1
+ app_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "hi"
+ }
+ android_resources_language_id {
+ language_code: "hi"
+ }
+ }
+ content_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "hi"
+ }
+ }
+ audio_translation_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "hi"
+ }
+ }
+}
+language_definitions {
+ language: HINGLISH
+ fallback_macro_language: ENGLISH
+ min_android_sdk_version: 1
+ content_string_id {
+ macaronic_id {
+ combined_language_code: "hi-en"
+ }
+ }
+ audio_translation_id {
+ macaronic_id {
+ combined_language_code: "hi-en"
+ }
+ }
+}
+language_definitions {
+ language: PORTUGUESE
+ min_android_sdk_version: 1
+}
+language_definitions {
+ language: BRAZILIAN_PORTUGUESE
+ fallback_macro_language: PORTUGUESE
+ min_android_sdk_version: 1
+ app_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "pt-BR"
+ }
+ android_resources_language_id {
+ language_code: "pt"
+ region_code: "BR"
+ }
+ }
+ content_string_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "pt-BR"
+ }
+ }
+ audio_translation_id {
+ ietf_bcp47_id {
+ ietf_language_tag: "pt-BR"
+ }
+ }
+}
diff --git a/config/src/java/org/oppia/android/config/languages/supported_regions.textproto b/config/src/java/org/oppia/android/config/languages/supported_regions.textproto
new file mode 100644
index 00000000000..14e223355ad
--- /dev/null
+++ b/config/src/java/org/oppia/android/config/languages/supported_regions.textproto
@@ -0,0 +1,23 @@
+region_definitions {
+ region: BRAZIL
+ region_id {
+ ietf_region_tag: "BR"
+ }
+ languages: PORTUGUESE
+ languages: BRAZILIAN_PORTUGUESE
+}
+region_definitions {
+ region: INDIA
+ region_id {
+ ietf_region_tag: "IN"
+ }
+ languages: HINDI
+ languages: HINGLISH
+}
+region_definitions {
+ region: UNITED_STATES
+ region_id {
+ ietf_region_tag: "US"
+ }
+ languages: ENGLISH
+}
diff --git a/data/build.gradle b/data/build.gradle
index 33aea201ef4..245404d1cd7 100644
--- a/data/build.gradle
+++ b/data/build.gradle
@@ -66,7 +66,7 @@ dependencies {
'androidx.appcompat:appcompat:1.0.2',
'com.android.support:multidex:1.0.3',
'com.google.dagger:dagger:2.24',
- 'com.google.protobuf:protobuf-lite:3.0.0',
+ 'com.google.protobuf:protobuf-javalite:3.17.3',
'com.squareup.moshi:moshi-kotlin:1.11.0',
'com.squareup.okhttp3:okhttp:4.1.0',
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2',
diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel
index b291178ff02..f481758ff28 100755
--- a/domain/BUILD.bazel
+++ b/domain/BUILD.bazel
@@ -8,6 +8,20 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
load("//domain:domain_assets.bzl", "generate_assets_list_from_text_protos")
load("//domain:domain_test.bzl", "domain_test")
+filegroup(
+ name = "test_manifest",
+ srcs = ["src/test/AndroidManifest.xml"],
+ visibility = ["//:oppia_testing_visibility"],
+)
+
+# Visibility for migrated domain tests.
+package_group(
+ name = "domain_testing_visibility",
+ packages = [
+ "//domain/src/test/...",
+ ],
+)
+
# Source files that have been migrated to their own package-based libraries. Files added to this
# list will be excluded automatically from the top-level utility library. It is recommended to use
# globs here to ensure that new files added to migrated packages don't accidentally get included in the
@@ -116,6 +130,15 @@ kt_android_library(
],
)
+android_library(
+ name = "test_resources",
+ testonly = True,
+ custom_package = "org.oppia.android.domain",
+ manifest = ":test_manifest",
+ resource_files = glob(["src/test/res/**"]),
+ visibility = [":domain_testing_visibility"],
+)
+
# TODO(#2143): Move InteractionObjectTestBuilder to a testing package outside the test folder.
kt_android_library(
name = "interaction_object_test_builder",
@@ -157,6 +180,7 @@ TEST_DEPS = [
"//third_party:org_jetbrains_kotlin_kotlin-reflect",
"//third_party:org_jetbrains_kotlin_kotlin-test-junit",
"//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
"//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module",
"//utility/src/main/java/org/oppia/android/util/extensions:context_extensions",
"//utility/src/main/java/org/oppia/android/util/networking:debug_module",
diff --git a/domain/build.gradle b/domain/build.gradle
index 1acc9a9798d..e27fe01d501 100644
--- a/domain/build.gradle
+++ b/domain/build.gradle
@@ -61,6 +61,26 @@ android {
}
}
+// These tests aren't supported in Gradle since they make use of test resources & AGP doesn't
+// support merging resources for test builds, or they rely on a compile-time built proto
+// configuration file which the current Gradle configuration doesn't support. This test runs
+// correctly for Bazel & is included in the CI workflow that runs Bazel tests.
+// https://stackoverflow.com/a/69141612 seems like the only solution that actually works (versus
+// trying to exclude via sourceSets), so the following is an adapted version that ensures all
+// generated sources that may reference the test also don't exist (such as Dagger running to
+// generate a test application component). Note that this must exist in tandem with the sourceSet
+// exclusion in order to properly work.
+def filesToExclude = [
+ '**/*DisplayLocaleImplTest*.kt',
+ '**/*LanguageConfigRetrieverTest*.kt',
+ '**/*LocaleControllerTest*.kt',
+ '**/*TranslationControllerTest*.kt'
+]
+tasks.withType(org.gradle.api.tasks.SourceTask.class).configureEach {
+ it.exclude(filesToExclude)
+}
+android.sourceSets.test.kotlin.exclude(filesToExclude)
+
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation(
@@ -72,6 +92,7 @@ dependencies {
'com.google.firebase:firebase-analytics-ktx:17.5.0',
'com.google.firebase:firebase-crashlytics:17.0.0',
'com.google.guava:guava:28.1-android',
+ 'com.google.protobuf:protobuf-javalite:3.17.3',
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
)
compileOnly(
diff --git a/domain/src/main/java/org/oppia/android/domain/locale/AndroidLocaleFactory.kt b/domain/src/main/java/org/oppia/android/domain/locale/AndroidLocaleFactory.kt
new file mode 100644
index 00000000000..c66235c4d30
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/locale/AndroidLocaleFactory.kt
@@ -0,0 +1,151 @@
+package org.oppia.android.domain.locale
+
+import android.os.Build
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.oppia.android.util.locale.AndroidLocaleProfile
+import org.oppia.android.util.locale.OppiaLocale
+import org.oppia.android.util.locale.getFallbackLanguageId
+import org.oppia.android.util.locale.getLanguageId
+import java.util.Locale
+import javax.inject.Inject
+
+/**
+ * Factory for creating new Android [Locale]s. This is meant only to be used within the locale
+ * domain package.
+ */
+class AndroidLocaleFactory @Inject constructor(
+ private val machineLocale: OppiaLocale.MachineLocale
+) {
+ /**
+ * Returns a new [Locale] that matches the given [OppiaLocaleContext]. Note this will
+ * automatically fail over to the context's backup fallback language if the primary language
+ * doesn't match any available locales on the device. Further, if no locale can be found, the
+ * returned [Locale] will be forced to match the specified context (which will result in some
+ * default/root locale behavior in Android).
+ */
+ fun createAndroidLocale(localeContext: OppiaLocaleContext): Locale {
+ val languageId = localeContext.getLanguageId()
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // Locale is always computed based on the Android resource app string identifier if that's
+ // defined. If it isn't, the routine falls back to app language & region country codes (which
+ // also provides interoperability with system-derived contexts). Note that if either identifier
+ // is missing for the primary language, the fallback is used instead (if available), except that
+ // IETF BCP 47 tags from the primary language are used before Android resource codes from the
+ // fallback. Thus, the order of this list is important. Finally, a basic check is done here to
+ // make sure this version of Android can actually render the target language.
+ val potentialProfiles =
+ computePotentialLanguageProfiles(localeContext, languageId) +
+ computePotentialFallbackLanguageProfiles(localeContext, fallbackLanguageId)
+
+ // Either find the first supported profile or force the locale to use the exact definition
+ // values, depending on whether to fail over to a forced locale.
+ val firstSupportedProfile = potentialProfiles.findFirstSupported()
+ val selectedProfile = firstSupportedProfile
+ ?: languageId.computeForcedProfile(localeContext.regionDefinition)
+ return Locale(selectedProfile.languageCode, selectedProfile.getNonWildcardRegionCode())
+ }
+
+ private fun computePotentialLanguageProfiles(
+ localeContext: OppiaLocaleContext,
+ languageId: LanguageId
+ ): List =
+ computeLanguageProfiles(localeContext, localeContext.languageDefinition, languageId)
+
+ private fun computePotentialFallbackLanguageProfiles(
+ localeContext: OppiaLocaleContext,
+ fallbackLanguageId: LanguageId
+ ): List {
+ return computeLanguageProfiles(
+ localeContext, localeContext.fallbackLanguageDefinition, fallbackLanguageId
+ )
+ }
+
+ private fun computeLanguageProfiles(
+ localeContext: OppiaLocaleContext,
+ definition: LanguageSupportDefinition,
+ languageId: LanguageId
+ ): List {
+ return if (definition.minAndroidSdkVersion <= Build.VERSION.SDK_INT) {
+ listOfNotNull(
+ languageId.computeLocaleProfileFromAndroidId(),
+ AndroidLocaleProfile.createFromIetfDefinitions(languageId, localeContext.regionDefinition),
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageId)
+ )
+ } else listOf()
+ }
+
+ private fun LanguageId.computeLocaleProfileFromAndroidId(): AndroidLocaleProfile? {
+ return if (hasAndroidResourcesLanguageId()) {
+ androidResourcesLanguageId.run {
+ // Empty region codes are allowed for Android resource IDs since they should always be used
+ // verbatim to ensure the correct Android resource string can be computed (such as for macro
+ // languages).
+ maybeConstructProfileWithWildcardSupport(languageCode, regionCode)
+ }
+ } else null
+ }
+
+ /**
+ * Returns an [AndroidLocaleProfile] for this [LanguageId] and the specified
+ * [RegionSupportDefinition] based on the language's & region's IETF BCP 47 codes regardless of
+ * whether they're defined (i.e. it's fine to default to empty string here since that will
+ * leverage Android's own root locale behavior).
+ */
+ private fun LanguageId.computeForcedProfile(
+ regionDefinition: RegionSupportDefinition
+ ): AndroidLocaleProfile {
+ if (hasAndroidResourcesLanguageId()) {
+ // Create a locale exactly matching the Android ID profile.
+ return AndroidLocaleProfile(
+ androidResourcesLanguageId.languageCode, androidResourcesLanguageId.regionCode
+ )
+ }
+ return when (languageTypeCase) {
+ LanguageId.LanguageTypeCase.IETF_BCP47_ID -> {
+ AndroidLocaleProfile(
+ ietfBcp47Id.ietfLanguageTag, regionDefinition.regionId.ietfRegionTag
+ )
+ }
+ LanguageId.LanguageTypeCase.MACARONIC_ID -> {
+ AndroidLocaleProfile.createFromMacaronicLanguage(this)
+ ?: error("Invalid macaronic ID: ${macaronicId.combinedLanguageCode}")
+ }
+ LanguageId.LanguageTypeCase.LANGUAGETYPE_NOT_SET, null ->
+ error("Invalid language case: $languageTypeCase")
+ }
+ }
+
+ private fun maybeConstructProfileWithWildcardSupport(
+ languageCode: String,
+ regionCode: String
+ ): AndroidLocaleProfile? {
+ return if (languageCode.isNotEmpty()) {
+ val adjustedRegionCode = if (regionCode.isEmpty()) {
+ AndroidLocaleProfile.REGION_WILDCARD
+ } else regionCode
+ AndroidLocaleProfile(languageCode, adjustedRegionCode)
+ } else null
+ }
+
+ private fun List.findFirstSupported(): AndroidLocaleProfile? = find {
+ availableLocaleProfiles.any { availableProfile ->
+ availableProfile.matches(machineLocale, it)
+ }
+ }
+
+ private companion object {
+ private val availableLocaleProfiles by lazy {
+ Locale.getAvailableLocales().map(AndroidLocaleProfile::createFrom)
+ }
+
+ private fun AndroidLocaleProfile.getNonWildcardRegionCode(): String {
+ return if (regionCode != AndroidLocaleProfile.REGION_WILDCARD) {
+ regionCode
+ } else ""
+ }
+ }
+}
diff --git a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel
new file mode 100644
index 00000000000..6fe929306cc
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel
@@ -0,0 +1,89 @@
+"""
+Domain definitions for managing languages & locales.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
+
+kt_android_library(
+ name = "locale_controller",
+ srcs = [
+ "LocaleController.kt",
+ ],
+ visibility = ["//:oppia_api_visibility"],
+ deps = [
+ ":content_locale_impl",
+ ":dagger",
+ ":display_locale_impl",
+ ":language_config_retriever",
+ "//domain",
+ "//model:languages_java_proto_lite",
+ "//utility/src/main/java/org/oppia/android/util/data:async_result",
+ "//utility/src/main/java/org/oppia/android/util/data:data_provider",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale",
+ ],
+)
+
+kt_android_library(
+ name = "android_locale_factory",
+ srcs = [
+ "AndroidLocaleFactory.kt",
+ ],
+ visibility = [
+ "//domain:domain_testing_visibility",
+ ],
+ deps = [
+ ":dagger",
+ "//utility/src/main/java/org/oppia/android/util/locale:android_locale_profile",
+ ],
+)
+
+kt_android_library(
+ name = "display_locale_impl",
+ srcs = [
+ "DisplayLocaleImpl.kt",
+ ],
+ visibility = [
+ "//domain:domain_testing_visibility",
+ ],
+ deps = [
+ ":android_locale_factory",
+ "//third_party:androidx_core_core",
+ "//utility/src/main/java/org/oppia/android/util/data:data_providers",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_bidi_formatter",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale",
+ ],
+)
+
+kt_android_library(
+ name = "content_locale_impl",
+ srcs = [
+ "ContentLocaleImpl.kt",
+ ],
+ visibility = [
+ "//domain:domain_testing_visibility",
+ ],
+ deps = [
+ "//model:languages_java_proto_lite",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale",
+ ],
+)
+
+kt_android_library(
+ name = "language_config_retriever",
+ srcs = [
+ "LanguageConfigRetriever.kt",
+ ],
+ visibility = [
+ "//domain:domain_testing_visibility",
+ ],
+ deps = [
+ ":dagger",
+ "//config/src/java/org/oppia/android/config:languages_config",
+ "//model:languages_java_proto_lite",
+ "//utility/src/main/java/org/oppia/android/util/caching:annotations",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
+ ],
+)
+
+dagger_rules()
diff --git a/domain/src/main/java/org/oppia/android/domain/locale/ContentLocaleImpl.kt b/domain/src/main/java/org/oppia/android/domain/locale/ContentLocaleImpl.kt
new file mode 100644
index 00000000000..7944c245d51
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/locale/ContentLocaleImpl.kt
@@ -0,0 +1,13 @@
+package org.oppia.android.domain.locale
+
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.util.locale.OppiaLocale
+
+/**
+ * A data class implementation of [OppiaLocale.ContentLocale].
+ *
+ * @property oppiaLocaleContext the [OppiaLocaleContext] corresponding to this locale
+ */
+data class ContentLocaleImpl(
+ val oppiaLocaleContext: OppiaLocaleContext
+) : OppiaLocale.ContentLocale(oppiaLocaleContext)
diff --git a/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt
new file mode 100644
index 00000000000..39b231cf4ac
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt
@@ -0,0 +1,120 @@
+package org.oppia.android.domain.locale
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import androidx.annotation.ArrayRes
+import androidx.annotation.StringRes
+import androidx.core.text.TextUtilsCompat
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.util.locale.OppiaBidiFormatter
+import org.oppia.android.util.locale.OppiaLocale
+import java.text.DateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.Objects
+
+// TODO(#3766): Restrict to be 'internal'.
+/** Implementation of [OppiaLocale.DisplayLocale]. */
+class DisplayLocaleImpl(
+ localeContext: OppiaLocaleContext,
+ private val machineLocale: MachineLocale,
+ private val androidLocaleFactory: AndroidLocaleFactory,
+ private val formatterFactory: OppiaBidiFormatter.Factory
+) : OppiaLocale.DisplayLocale(localeContext) {
+ // TODO(#3766): Restrict to be 'internal'.
+ /** The [Locale] used for user-facing string formatting in this display locale. */
+ val formattingLocale: Locale by lazy { androidLocaleFactory.createAndroidLocale(localeContext) }
+ private val dateFormat by lazy {
+ DateFormat.getDateInstance(DATE_FORMAT_LENGTH, formattingLocale)
+ }
+ private val timeFormat by lazy {
+ DateFormat.getTimeInstance(TIME_FORMAT_LENGTH, formattingLocale)
+ }
+ private val dateTimeFormat by lazy {
+ DateFormat.getDateTimeInstance(DATE_FORMAT_LENGTH, TIME_FORMAT_LENGTH, formattingLocale)
+ }
+ private val bidiFormatter by lazy { formatterFactory.createFormatter(formattingLocale) }
+
+ // TODO(#3766): Restrict to be 'internal'.
+ /**
+ * Updates the specified [Configuration] to reference the formatting locale backing this display
+ * locale. Note that this may not be sufficient for actually updating the configuration--it may
+ * need to be done during activity initialization and in some cases an activity recreation may be
+ * necessary.
+ */
+ fun setAsDefault(configuration: Configuration) {
+ configuration.setLocale(formattingLocale)
+ }
+
+ override fun computeDateString(timestampMillis: Long): String =
+ dateFormat.format(Date(timestampMillis))
+
+ override fun computeTimeString(timestampMillis: Long): String =
+ timeFormat.format(Date(timestampMillis))
+
+ override fun computeDateTimeString(timestampMillis: Long): String =
+ dateTimeFormat.format(Date(timestampMillis))
+
+ override fun getLayoutDirection(): Int {
+ return TextUtilsCompat.getLayoutDirectionFromLocale(formattingLocale)
+ }
+
+ override fun String.formatInLocaleWithWrapping(vararg args: CharSequence): String {
+ return formatInLocaleWithoutWrapping(
+ *args.map { arg -> bidiFormatter.wrapText(arg) }.toTypedArray()
+ )
+ }
+
+ override fun String.formatInLocaleWithoutWrapping(vararg args: CharSequence): String =
+ format(formattingLocale, *args)
+
+ override fun String.capitalizeForHumans(): String = capitalize(formattingLocale)
+
+ override fun Resources.getStringInLocale(@StringRes id: Int): String = getString(id)
+
+ override fun Resources.getStringInLocaleWithWrapping(
+ id: Int,
+ vararg formatArgs: CharSequence
+ ): String = getStringInLocale(id).formatInLocaleWithWrapping(*formatArgs)
+
+ override fun Resources.getStringInLocaleWithoutWrapping(
+ id: Int,
+ vararg formatArgs: CharSequence
+ ): String = getStringInLocale(id).formatInLocaleWithoutWrapping(*formatArgs)
+
+ override fun Resources.getStringArrayInLocale(@ArrayRes id: Int): List =
+ getStringArray(id).toList()
+
+ override fun Resources.getQuantityStringInLocale(id: Int, quantity: Int): String =
+ getQuantityTextInLocale(id, quantity).toString()
+
+ override fun Resources.getQuantityStringInLocaleWithWrapping(
+ id: Int,
+ quantity: Int,
+ vararg formatArgs: CharSequence
+ ): String = getQuantityStringInLocale(id, quantity).formatInLocaleWithWrapping(*formatArgs)
+
+ override fun Resources.getQuantityStringInLocaleWithoutWrapping(
+ id: Int,
+ quantity: Int,
+ vararg formatArgs: CharSequence
+ ): String = getQuantityStringInLocale(id, quantity).formatInLocaleWithoutWrapping(*formatArgs)
+
+ override fun Resources.getQuantityTextInLocale(id: Int, quantity: Int): CharSequence =
+ getQuantityText(id, quantity)
+
+ override fun toString(): String = "DisplayLocaleImpl[context=$localeContext]"
+
+ override fun equals(other: Any?): Boolean {
+ return (other as? DisplayLocaleImpl)?.let { locale ->
+ localeContext == locale.localeContext && machineLocale == locale.machineLocale
+ } ?: false
+ }
+
+ override fun hashCode(): Int = Objects.hash(localeContext, machineLocale)
+
+ private companion object {
+ private const val DATE_FORMAT_LENGTH = DateFormat.LONG
+ private const val TIME_FORMAT_LENGTH = DateFormat.SHORT
+ }
+}
diff --git a/domain/src/main/java/org/oppia/android/domain/locale/LanguageConfigRetriever.kt b/domain/src/main/java/org/oppia/android/domain/locale/LanguageConfigRetriever.kt
new file mode 100644
index 00000000000..ca9df28fcbd
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/locale/LanguageConfigRetriever.kt
@@ -0,0 +1,29 @@
+package org.oppia.android.domain.locale
+
+import org.oppia.android.app.model.SupportedLanguages
+import org.oppia.android.app.model.SupportedRegions
+import org.oppia.android.util.caching.AssetRepository
+import javax.inject.Inject
+
+/**
+ * Retriever for language configurations from the app's embedded assets.
+ *
+ * Note that this implementation is expected to no-op on Gradle builds since they don't include the
+ * necessary configuration files. The rest of the locale & translation systems are expected to
+ * gracefully fail in this case.
+ */
+class LanguageConfigRetriever @Inject constructor(private val assetRepository: AssetRepository) {
+ /** Returns the [SupportedLanguages] configuration for the app, or default instance if none. */
+ fun loadSupportedLanguages(): SupportedLanguages {
+ return assetRepository.tryLoadProtoFromLocalAssets(
+ "supported_languages", SupportedLanguages.getDefaultInstance()
+ )
+ }
+
+ /** Returns the [SupportedRegions] configuration for the app, or default instance if none. */
+ fun loadSupportedRegions(): SupportedRegions {
+ return assetRepository.tryLoadProtoFromLocalAssets(
+ "supported_regions", SupportedRegions.getDefaultInstance()
+ )
+ }
+}
diff --git a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt
new file mode 100644
index 00000000000..351eee9da50
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt
@@ -0,0 +1,468 @@
+package org.oppia.android.domain.locale
+
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.res.Configuration
+import android.os.Build
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.UNRECOGNIZED
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.USAGE_MODE_UNSPECIFIED
+import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.oppia.android.app.model.SupportedLanguages
+import org.oppia.android.app.model.SupportedRegions
+import org.oppia.android.domain.oppialogger.OppiaLogger
+import org.oppia.android.util.data.AsyncDataSubscriptionManager
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProvider
+import org.oppia.android.util.data.DataProviders
+import org.oppia.android.util.data.DataProviders.Companion.transformAsync
+import org.oppia.android.util.locale.AndroidLocaleProfile
+import org.oppia.android.util.locale.OppiaBidiFormatter
+import org.oppia.android.util.locale.OppiaLocale
+import org.oppia.android.util.locale.OppiaLocale.ContentLocale
+import org.oppia.android.util.locale.OppiaLocale.DisplayLocale
+import org.oppia.android.util.locale.OppiaLocale.MachineLocale
+import java.util.Locale
+import java.util.concurrent.locks.ReentrantLock
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.concurrent.withLock
+
+private const val ANDROID_SYSTEM_LOCALE_DATA_PROVIDER_ID = "android_locale"
+private const val APP_STRING_LOCALE_DATA_BASE_PROVIDER_ID = "app_string_locale."
+private const val WRITTEN_TRANSLATION_LOCALE_BASE_DATA_PROVIDER_ID = "written_translation_locale."
+private const val AUDIO_TRANSLATIONS_LOCALE_BASE_DATA_PROVIDER_ID = "audio_translations_locale."
+private const val SYSTEM_LANGUAGE_DATA_PROVIDER_ID = "system_language"
+
+/** Controller for creating & retrieving user-specified [OppiaLocale]s. */
+@Singleton
+class LocaleController @Inject constructor(
+ private val applicationContext: Context,
+ private val dataProviders: DataProviders,
+ private val languageConfigRetriever: LanguageConfigRetriever,
+ private val oppiaLogger: OppiaLogger,
+ private val asyncDataSubscriptionManager: AsyncDataSubscriptionManager,
+ private val machineLocale: MachineLocale,
+ private val androidLocaleFactory: AndroidLocaleFactory,
+ private val formatterFactory: OppiaBidiFormatter.Factory
+) {
+ private val definitionsLock = ReentrantLock()
+ private lateinit var supportedLanguages: SupportedLanguages
+ private lateinit var supportedRegions: SupportedRegions
+
+ /**
+ * Returns the [OppiaLocaleContext] which provides a reasonable default context when building a
+ * locale for app strings. Generally this should be used in conjunction with
+ * [reconstituteDisplayLocale], and only during locale bootstrapping. App layer classes should
+ * rely on a consistent locale fetched via one of the data providers below rather than creating an
+ * immediate locale using these methods.
+ *
+ * Note that the returned locale cannot be assumed to follow any particular locale, only that it's
+ * likely to correspond to a valid Android locale when setting up an activity's configuration for
+ * string resource selection and layout arrangement.
+ */
+ fun getLikelyDefaultAppStringLocaleContext(): OppiaLocaleContext {
+ return OppiaLocaleContext.newBuilder().apply {
+ // Assume English for the default language since it has the highest chance of being
+ // successful. Note that this theoretically could differ from the language definitions
+ // since it's hardcoded, but that should be fine. Also, only assume app language
+ // support.
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.ENGLISH
+ minAndroidSdkVersion = 1
+ appStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "en"
+ }.build()
+ }.build()
+ }.build()
+ regionDefinition = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.UNITED_STATES
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "US"
+ }.build()
+ addLanguages(OppiaLanguage.ENGLISH)
+ }.build()
+ usageMode = APP_STRINGS
+ }.build()
+ }
+
+ // TODO(#3800): Utilize this method to build in resilience for low-memory process deaths resulting
+ // in crashes upon the app be foregrounded.
+ /**
+ * Returns a new [DisplayLocale] corresponding to the specified [OppiaLocaleContext]. This is
+ * meant to be used in cases when a context needs to be saved (e.g. in a bundle) and later
+ * restored without needing to synchronously re-fetch the locale via a data provider.
+ *
+ * [DisplayLocale] implementations guarantee functional equivalence between different instances so
+ * long as they have the same context, so the fetch is only needed for eventual consistency (i.e.
+ * for cases in which the user changed their selected language).
+ */
+ fun reconstituteDisplayLocale(oppiaLocaleContext: OppiaLocaleContext): DisplayLocale {
+ return DisplayLocaleImpl(
+ oppiaLocaleContext, machineLocale, androidLocaleFactory, formatterFactory
+ )
+ }
+
+ /**
+ * Notifies that the default Android locale may have changed.
+ *
+ * Since there's no way to reliably get notified when the default system locale is changed, it's
+ * the expected responsibility of calling infrastructure to ensure detected changes to the system
+ * locale (such as per an activity configuration change) result in downstream data providers being
+ * recomputed in case they rely on the system locale & the locale has indeed changed. See
+ * [retrieveSystemLanguage] & other provider methods in this class.
+ *
+ * This generally shouldn't be called broadly.
+ */
+ fun notifyPotentialLocaleChange() {
+ // There may not actually be a change in the default locale, but assume there is & trigger an
+ // update for data providers.
+ asyncDataSubscriptionManager.notifyChangeAsync(ANDROID_SYSTEM_LOCALE_DATA_PROVIDER_ID)
+ }
+
+ /**
+ * Returns the [DisplayLocale] corresponding to the specified [OppiaLanguage], or potentially a
+ * best-match locale that should be compatible with both the supported languages of the app and
+ * the system (though this isn't guaranteed). Cases in which no match can be determined will
+ * result in a failed result.
+ *
+ * Note that the returned [DataProvider] may be dependent on the system locale in which case its
+ * subscribers may be notified whenever callers notify this controller of potential system locale
+ * changes (i.e. via [notifyPotentialLocaleChange].
+ *
+ * The returned [OppiaLocale] will have an [OppiaLocaleContext] tied to app strings. Further, no
+ * assumptions can be made about the region information within [OppiaLocaleContext]. This
+ * controller makes little attempt to actually determine the region the user is in, and oftentimes
+ * will default to an unspecified region. This is largely because the main signal the app has for
+ * determining the user's region is the system locale (which is configurable by the user and not
+ * at all dependent on their geolocation). Other measures could be taken to try and get the user's
+ * location (such as using the telephony services), but these are limited (require cellular
+ * functionality & connectivity) and generally aren't needed for any decision making in the app
+ * (language is sufficient). Finally, it's possible for the region to change based on selecting
+ * different languages since the app may actually force the system locale into a specific region
+ * for properly app string localization (such as Brazil for Brazilian Portuguese).
+ */
+ fun retrieveAppStringDisplayLocale(language: OppiaLanguage): DataProvider {
+ val providerId = "$APP_STRING_LOCALE_DATA_BASE_PROVIDER_ID.${language.name}"
+ return getSystemLocaleProfile().transformAsync(providerId) { systemLocaleProfile ->
+ computeLocaleResult(language, systemLocaleProfile, APP_STRINGS)
+ }
+ }
+
+ /**
+ * Returns the [ContentLocale] corresponding to the specified [OppiaLanguage] (which is
+ * guaranteed to be supported by the app) to be used for written translations, or a failure if for
+ * some reason that's not possible (such as if the loaded language configuration doesn't include
+ * the specified language).
+ *
+ * The returned [DataProvider] has the same notification caveat as
+ * [retrieveAppStringDisplayLocale].
+ *
+ * The returned [OppiaLocale] will have an [OppiaLocaleContext] tied to content strings. The
+ * returned locale has the same region caveats as [retrieveAppStringDisplayLocale].
+ */
+ fun retrieveWrittenTranslationsLocale(language: OppiaLanguage): DataProvider {
+ val providerId = "$WRITTEN_TRANSLATION_LOCALE_BASE_DATA_PROVIDER_ID.${language.name}"
+ return getSystemLocaleProfile().transformAsync(providerId) { systemLocaleProfile ->
+ computeLocaleResult(language, systemLocaleProfile, CONTENT_STRINGS)
+ }
+ }
+
+ /**
+ * Returns the [ContentLocale] corresponding to the specified [OppiaLanguage] for audio
+ * translations with the same failure stipulation as [retrieveWrittenTranslationsLocale].
+ *
+ * The returned [DataProvider] has the same notification caveat as
+ * [retrieveAppStringDisplayLocale].
+ *
+ * The returned [OppiaLocale] will have an [OppiaLocaleContext] tied to audio translations. The
+ * returned locale has the same region caveats as [retrieveAppStringDisplayLocale].
+ */
+ fun retrieveAudioTranslationsLocale(language: OppiaLanguage): DataProvider {
+ val providerId = "$AUDIO_TRANSLATIONS_LOCALE_BASE_DATA_PROVIDER_ID.${language.name}"
+ return getSystemLocaleProfile().transformAsync(providerId) { systemLocaleProfile ->
+ computeLocaleResult(language, systemLocaleProfile, AUDIO_TRANSLATIONS)
+ }
+ }
+
+ /**
+ * Returns the [OppiaLanguage] best matching the current system locale (based on the list of
+ * supported languages by the app), or [OppiaLanguage.LANGUAGE_UNSPECIFIED] if the current system
+ * locale does not correspond to a supported language.
+ *
+ * Note that the system locale is only ever matched against app language definitions, never
+ * written or audio content translations.
+ *
+ * The returned [DataProvider]'s subscribers may be notified upon calls to
+ * [notifyPotentialLocaleChange] if there's actually a change in the system locale.
+ */
+ fun retrieveSystemLanguage(): DataProvider {
+ val providerId = SYSTEM_LANGUAGE_DATA_PROVIDER_ID
+ return getSystemLocaleProfile().transformAsync(providerId) { systemLocaleProfile ->
+ AsyncResult.success(
+ retrieveLanguageDefinitionFromSystemCode(systemLocaleProfile)?.language
+ ?: OppiaLanguage.LANGUAGE_UNSPECIFIED
+ )
+ }
+ }
+
+ /**
+ * Updates both the system and the specified [Configuration] to use the [DisplayLocale] as the
+ * default locale for the current app process (which will affect string resource retrieval).
+ *
+ * Note that this may result in data providers returned by this class being notified of changes if
+ * any depend on the current system locale, and will likely change the result of the data provider
+ * returned by [retrieveSystemLanguage].
+ */
+ fun setAsDefault(displayLocale: DisplayLocale, configuration: Configuration) {
+ (displayLocale as? DisplayLocaleImpl)?.let { locale ->
+ locale.setAsDefault(configuration)
+
+ // Note that this seemingly causes an infinite loop since notification happens in response to
+ // the upstream locale data provider changing, but it should terminate since the
+ // DataProvider->LiveData bridge only notifies if data changes (i.e. if the locale is actually
+ // new). Note also that this controller intentionally doesn't cache the system locale since
+ // there's no way to actually observe changes to it, so the controller aims to have eventual
+ // consistency by always retrieving the latest state when requested. This does mean locale
+ // changes can be missed if they aren't accompanied by a configuration change or activity
+ // recreation.
+ Locale.setDefault(locale.formattingLocale)
+
+ // Ensure that the application context is also using the new locale (which it should by
+ // default since Android's responsible for setting it, but this is done for assurance since
+ // other code in this controller relies on the application context's locale rather than the
+ // system default). Note also that this has the side effect of overriding user-selected
+ // locales for fallback languages (which is probably fine since the app relies on its own
+ // fallback mechanism when choosing the primary locale).
+ applicationContext.resources.configuration.setLocale(locale.formattingLocale)
+
+ notifyPotentialLocaleChange()
+ } ?: error("Invalid display locale type passed in: $displayLocale")
+ }
+
+ private fun getSystemLocaleProfile(): DataProvider {
+ return dataProviders.createInMemoryDataProvider(ANDROID_SYSTEM_LOCALE_DATA_PROVIDER_ID) {
+ AndroidLocaleProfile.createFrom(getSystemLocale())
+ }
+ }
+
+ /**
+ * Returns the current system [Locale], as specified by the user or system. Note that this
+ * generally prefers pulling from the application context since the app overwrites the static
+ * singleton Locale for the app.
+ */
+ @SuppressLint("ObsoleteSdkInt") // Incorrect warning since the app has a lower min sdk.
+ private fun getSystemLocale(): Locale {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ getDefaultLocaleApi24(applicationContext.resources.configuration)
+ } else getDefaultLocale(applicationContext.resources.configuration)
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private fun getDefaultLocaleApi24(configuration: Configuration): Locale {
+ val locales = configuration.locales
+ // Note that if this ever defaults to Locale.getDefault() it will break language switching when
+ // the user indicates that the app should use the system language (without restarting the app).
+ // Also, this only matches against the first locale. In the future, some effort could be made to
+ // try and pick the best matching system locale (per the user's preferences) rather than the
+ // "first or nothing" currently implemented here.
+ return if (locales.isEmpty) {
+ oppiaLogger.e(
+ "LocaleController",
+ "No locales defined for application context. Defaulting to default Locale."
+ )
+ Locale.getDefault()
+ } else locales[0]
+ }
+
+ @Suppress("DEPRECATION") // Old API is needed for SDK versions < N.
+ private fun getDefaultLocale(configuration: Configuration): Locale = configuration.locale
+
+ private suspend fun computeLocaleResult(
+ language: OppiaLanguage,
+ systemLocaleProfile: AndroidLocaleProfile,
+ usageMode: LanguageUsageMode
+ ): AsyncResult {
+ // The safe-cast here is meant to ensure a strongly typed public API with robustness against
+ // internal weirdness that would lead to a wrong type being produced from the generic helpers.
+ // This shouldn't actually ever happen in practice, but this code gracefully fails to a null
+ // (and thus a failure).
+ @Suppress("UNCHECKED_CAST") // as? should always be a safe cast, even if unchecked.
+ val locale = computeLocale(language, systemLocaleProfile, usageMode) as? T
+ return locale?.let {
+ AsyncResult.success(it)
+ } ?: AsyncResult.failed(
+ IllegalStateException(
+ "Language $language for usage $usageMode doesn't match supported language definitions"
+ )
+ )
+ }
+
+ private suspend fun computeLocale(
+ language: OppiaLanguage,
+ systemLocaleProfile: AndroidLocaleProfile,
+ usageMode: LanguageUsageMode
+ ): OppiaLocale? {
+ val localeContext = OppiaLocaleContext.newBuilder().apply {
+ languageDefinition =
+ computeLanguageDefinition(language, systemLocaleProfile, usageMode) ?: return null
+ retrieveLanguageDefinition(languageDefinition.fallbackMacroLanguage)?.let {
+ fallbackLanguageDefinition = it
+ }
+ regionDefinition = retrieveRegionDefinition(systemLocaleProfile.regionCode)
+ this.usageMode = usageMode
+ }.build()
+
+ // Check whether the selected language is actually expected for the user's region (it might not
+ // be, but the app should generally still behave correctly).
+ val selectedLanguage = localeContext.languageDefinition.language
+ val matchedRegion = localeContext.regionDefinition
+ if (selectedLanguage !in matchedRegion.languagesList) {
+ oppiaLogger.w(
+ "LocaleController",
+ "Notice: selected language $selectedLanguage is not part of the corresponding region" +
+ " matched to this locale: ${matchedRegion.region} (ID:" +
+ " ${matchedRegion.regionId.ietfRegionTag}) (supported languages:" +
+ " ${matchedRegion.languagesList}"
+ )
+ }
+
+ return when (usageMode) {
+ APP_STRINGS ->
+ DisplayLocaleImpl(localeContext, machineLocale, androidLocaleFactory, formatterFactory)
+ CONTENT_STRINGS, AUDIO_TRANSLATIONS -> ContentLocaleImpl(localeContext)
+ USAGE_MODE_UNSPECIFIED, UNRECOGNIZED -> null
+ }
+ }
+
+ private suspend fun computeLanguageDefinition(
+ language: OppiaLanguage,
+ systemLocaleProfile: AndroidLocaleProfile,
+ usageMode: LanguageUsageMode
+ ): LanguageSupportDefinition? {
+ // Matching behaves as follows (for app strings):
+ // 1. Try to find a matching definition directly for the language.
+ // 2. If that fails, try falling back to the current system language.
+ // 3. If that fails, create a basic definition to represent the system language.
+ // Content strings & audio translations only perform step 1 since there's no reasonable
+ // fallback.
+ val matchedDefinition = retrieveLanguageDefinition(language)
+ return if (usageMode == APP_STRINGS) {
+ matchedDefinition
+ ?: retrieveLanguageDefinitionFromSystemCode(systemLocaleProfile)
+ ?: computeDefaultLanguageDefinitionForSystemLanguage(systemLocaleProfile.languageCode)
+ } else matchedDefinition
+ }
+
+ /**
+ * Returns the [LanguageSupportDefinition] corresponding to the specified language, if it exists.
+ * In general, a definition should always exist unless the language is unspecified.
+ */
+ private suspend fun retrieveLanguageDefinition(
+ language: OppiaLanguage
+ ): LanguageSupportDefinition? {
+ val definitions = retrieveAllLanguageDefinitions()
+ return definitions.languageDefinitionsList.find {
+ it.language == language
+ }.also {
+ if (it == null) {
+ oppiaLogger.w("LocaleController", "Encountered unmatched language: $language")
+ }
+ }
+ }
+
+ /**
+ * Returns the [LanguageSupportDefinition] corresponding to the specified locale profile, or null
+ * if none match.
+ *
+ * This only matches against app string IDs since content & audio translations never fall back to
+ * system languages.
+ */
+ private suspend fun retrieveLanguageDefinitionFromSystemCode(
+ localeProfile: AndroidLocaleProfile
+ ): LanguageSupportDefinition? {
+ val definitions = retrieveAllLanguageDefinitions()
+ // Attempt to find a matching definition. Note that while Locale's language code is expected to
+ // be an ISO 639-1/2/3 code, it not necessarily match the IETF BCP 47 tag defined for this
+ // language. If a language is unknown, return a definition that attempts to be interoperable
+ // with Android. Note that a sequence is used here to avoid computing extra profiles when not
+ // needed.
+ return definitions.languageDefinitionsList.asSequence().mapNotNull { definition ->
+ return@mapNotNull definition.retrieveAppLanguageProfile()?.let { profile ->
+ profile to definition
+ }
+ }.find { (profile, _) ->
+ localeProfile.matches(machineLocale, profile)
+ }?.let { (_, definition) -> definition }
+ }
+
+ private suspend fun retrieveRegionDefinition(countryCode: String): RegionSupportDefinition {
+ val definitions = retrieveAllRegionDefinitions()
+ // Attempt to find a matching definition. Note that while Locale's country code can either be
+ // an ISO 3166 alpha-2 or UN M.49 numeric-3 code, that may not necessarily match the IETF BCP
+ // 47 tag defined for this region. If a region doesn't match, return unknown & just use the
+ // country code directly for the formatting locale.
+ return definitions.regionDefinitionsList.find {
+ machineLocale.run {
+ it.regionId.ietfRegionTag.equalsIgnoreCase(countryCode)
+ }
+ } ?: RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.REGION_UNSPECIFIED
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = countryCode
+ }.build()
+ }.build()
+ }
+
+ @Suppress("RedundantSuspendModifier") // Keep to force calls to background threads.
+ private suspend fun retrieveAllLanguageDefinitions() = definitionsLock.withLock {
+ if (!::supportedLanguages.isInitialized) {
+ supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+ }
+ return@withLock supportedLanguages
+ }
+
+ @Suppress("RedundantSuspendModifier") // Keep to force calls to background threads.
+ private suspend fun retrieveAllRegionDefinitions() = definitionsLock.withLock {
+ if (!::supportedRegions.isInitialized) {
+ supportedRegions = languageConfigRetriever.loadSupportedRegions()
+ }
+ return@withLock supportedRegions
+ }
+
+ private fun LanguageSupportDefinition.retrieveAppLanguageProfile(): AndroidLocaleProfile? {
+ return when (appStringId.languageTypeCase) {
+ LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID ->
+ AndroidLocaleProfile.createFromIetfDefinitions(appStringId, regionDefinition = null)
+ LanguageSupportDefinition.LanguageId.LanguageTypeCase.MACARONIC_ID -> {
+ // Likely won't match against system languages.
+ AndroidLocaleProfile.createFromMacaronicLanguage(appStringId)
+ }
+ LanguageSupportDefinition.LanguageId.LanguageTypeCase.LANGUAGETYPE_NOT_SET, null -> null
+ }
+ }
+
+ private fun computeDefaultLanguageDefinitionForSystemLanguage(
+ languageCode: String
+ ) = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED
+ minAndroidSdkVersion = 1 // Assume it's supported on the current version.
+ // Only app strings can be supported since this is a system language. Content & audio languages
+ // must be part of the language definitions. Support for app strings is exposed so that a locale
+ // can be constructed from it.
+ appStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = languageCode
+ }.build()
+ }.build()
+ }.build()
+}
diff --git a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel
new file mode 100644
index 00000000000..b03063bfffd
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel
@@ -0,0 +1,28 @@
+"""
+Domain definitions for managing translations.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
+
+kt_android_library(
+ name = "translation_controller",
+ srcs = [
+ "TranslationController.kt",
+ ],
+ visibility = ["//:oppia_api_visibility"],
+ deps = [
+ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller",
+ "//model:languages_java_proto_lite",
+ "//model:profile_java_proto_lite",
+ "//model:subtitled_html_java_proto_lite",
+ "//model:subtitled_unicode_java_proto_lite",
+ "//model:translation_java_proto_lite",
+ "//utility/src/main/java/org/oppia/android/util/data:async_result",
+ "//utility/src/main/java/org/oppia/android/util/data:data_provider",
+ "//utility/src/main/java/org/oppia/android/util/data:data_providers",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale",
+ ],
+)
+
+dagger_rules()
diff --git a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt
new file mode 100644
index 00000000000..537dd846b7e
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt
@@ -0,0 +1,340 @@
+package org.oppia.android.domain.translation
+
+import org.oppia.android.app.model.AppLanguageSelection
+import org.oppia.android.app.model.AudioTranslationLanguageSelection
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.SubtitledHtml
+import org.oppia.android.app.model.SubtitledUnicode
+import org.oppia.android.app.model.WrittenTranslationContext
+import org.oppia.android.app.model.WrittenTranslationLanguageSelection
+import org.oppia.android.domain.locale.LocaleController
+import org.oppia.android.util.data.AsyncDataSubscriptionManager
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProvider
+import org.oppia.android.util.data.DataProviders
+import org.oppia.android.util.data.DataProviders.Companion.transform
+import org.oppia.android.util.data.DataProviders.Companion.transformAsync
+import org.oppia.android.util.locale.OppiaLocale
+import java.util.concurrent.locks.ReentrantLock
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.concurrent.withLock
+
+private const val SYSTEM_LANGUAGE_LOCALE_DATA_PROVIDER_ID = "system_language_locale"
+private const val APP_LANGUAGE_DATA_PROVIDER_ID = "app_language"
+private const val APP_LANGUAGE_LOCALE_DATA_PROVIDER_ID = "app_language_locale"
+private const val UPDATE_APP_LANGUAGE_DATA_PROVIDER_ID = "update_app_language"
+private const val WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID = "written_translation_content"
+private const val WRITTEN_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID =
+ "written_translation_content_locale"
+private const val UPDATE_WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID =
+ "update_written_translation_content"
+private const val AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID = "audio_translation_content"
+private const val AUDIO_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID =
+ "audio_translation_content_locale"
+private const val UPDATE_AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID =
+ "update_audio_translation_content"
+
+/**
+ * Domain controller for performing operations corresponding to translations.
+ *
+ * This controller is often used instead of [LocaleController] since it provides additional
+ * functionality which simplifies interacting with the locales needed for various translation
+ * scenarios, but it relies on locale controller as its source of truth.
+ */
+@Singleton
+class TranslationController @Inject constructor(
+ private val dataProviders: DataProviders,
+ private val localeController: LocaleController,
+ private val asyncDataSubscriptionManager: AsyncDataSubscriptionManager
+) {
+ // TODO(#52): Finish this implementation. The implementation below doesn't actually save/restore
+ // settings from the local filesystem since the UI has been currently disabled as part of #20.
+ // Also, there should be a proper default locale for pre-profile selection (either a default
+ // app-wide setting determined by the administrator, or the last locale used by a profile)--more
+ // product & UX thought is needed here. Further, extra work is likely needed to handle the case
+ // when the user switches between a system & non-system language since the existing system
+ // defaulting behavior breaks down when the system locale is overwritten (such as when a
+ // different language is selected).
+
+ private val dataLock = ReentrantLock()
+ private val appLanguageSettings = mutableMapOf()
+ private val writtenTranslationLanguageSettings =
+ mutableMapOf()
+ private val audioVoiceoverLanguageSettings =
+ mutableMapOf()
+
+ /**
+ * Returns a data provider for an app string [OppiaLocale.DisplayLocale] corresponding to the
+ * current user-selected system language.
+ */
+ fun getSystemLanguageLocale(): DataProvider {
+ return getSystemLanguage().transformAsync(SYSTEM_LANGUAGE_LOCALE_DATA_PROVIDER_ID) { language ->
+ localeController.retrieveAppStringDisplayLocale(language).retrieveData()
+ }
+ }
+
+ /**
+ * Returns a data provider for the current [OppiaLanguage] selected for app strings for the
+ * specified user (per their [profileId]).
+ *
+ * This language can be updated via [updateAppLanguage].
+ */
+ fun getAppLanguage(profileId: ProfileId): DataProvider {
+ return getAppLanguageLocale(profileId).transform(APP_LANGUAGE_DATA_PROVIDER_ID) { locale ->
+ locale.getCurrentLanguage()
+ }
+ }
+
+ /**
+ * Returns a data provider for a [OppiaLocale.DisplayLocale] corresponding to the user's selected
+ * language for app strings (see [getAppLanguage]).
+ */
+ fun getAppLanguageLocale(profileId: ProfileId): DataProvider {
+ val providerId = APP_LANGUAGE_LOCALE_DATA_PROVIDER_ID
+ return getSystemLanguage().transformAsync(providerId) { systemLanguage ->
+ val language = computeAppLanguage(profileId, systemLanguage)
+ return@transformAsync localeController.retrieveAppStringDisplayLocale(language).retrieveData()
+ }
+ }
+
+ /**
+ * Updates the language to be used by the specified user for app string translations. Note that
+ * the provided [AppLanguageSelection] provides the user with the option of either selecting a
+ * specific supported language for app strings, or to fall back to the system default.
+ *
+ * The app guarantees app language compatibility for any non-system language selected, and a
+ * best-effort basis for translating strings for system languages (generally if the system
+ * language matches a supported language, otherwise the app defaults to English).
+ *
+ * @return a [DataProvider] which succeeds only if the update succeeds, otherwise fails (only one
+ * result is ever provided)
+ */
+ fun updateAppLanguage(profileId: ProfileId, selection: AppLanguageSelection): DataProvider {
+ return dataProviders.createInMemoryDataProviderAsync(UPDATE_APP_LANGUAGE_DATA_PROVIDER_ID) {
+ updateAppLanguageSelection(profileId, selection)
+ return@createInMemoryDataProviderAsync AsyncResult.success(Unit)
+ }
+ }
+
+ /**
+ * Returns a data provider for the current [OppiaLanguage] selected for written content strings
+ * for the specified user (per their [profileId]).
+ *
+ * This language can be updated via [updateWrittenTranslationContentLanguage].
+ */
+ fun getWrittenTranslationContentLanguage(profileId: ProfileId): DataProvider {
+ val providerId = WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID
+ return getWrittenTranslationContentLocale(profileId).transform(providerId) { locale ->
+ locale.getCurrentLanguage()
+ }
+ }
+
+ /**
+ * Returns a data provider for a [OppiaLocale.ContentLocale] corresponding to the user's selected
+ * language for written content strings (see [getWrittenTranslationContentLanguage]).
+ */
+ fun getWrittenTranslationContentLocale(
+ profileId: ProfileId
+ ): DataProvider {
+ val providerId = WRITTEN_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID
+ return getSystemLanguage().transformAsync(providerId) { systemLanguage ->
+ val language = computeWrittenTranslationContentLanguage(profileId, systemLanguage)
+ val writtenTranslationLocale = localeController.retrieveWrittenTranslationsLocale(language)
+ return@transformAsync writtenTranslationLocale.retrieveData()
+ }
+ }
+
+ /**
+ * Updates the language to be used by the specified user for written content string translations.
+ * Note that the provided [WrittenTranslationLanguageSelection] provides the user with the option
+ * of either selecting a specific supported language for content strings, or to fall back to
+ * whatever the app language selection is (which may be the system default).
+ *
+ * Note that the app guarantees a list of languages to support for written translations as a
+ * superset. The actual availability for a particular language is topic-dependent.
+ *
+ * @return a [DataProvider] which succeeds only if the update succeeds, otherwise fails (only one
+ * result is ever provided)
+ */
+ fun updateWrittenTranslationContentLanguage(
+ profileId: ProfileId,
+ selection: WrittenTranslationLanguageSelection
+ ): DataProvider {
+ val providerId = UPDATE_WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID
+ return dataProviders.createInMemoryDataProviderAsync(providerId) {
+ updateWrittenTranslationContentLanguageSelection(profileId, selection)
+ return@createInMemoryDataProviderAsync AsyncResult.success(Unit)
+ }
+ }
+
+ /**
+ * Returns a data provider for the current [OppiaLanguage] selected for audio voiceovers for the
+ * specified user (per their [profileId]).
+ *
+ * This language can be updated via [updateAudioTranslationContentLanguage].
+ */
+ fun getAudioTranslationContentLanguage(profileId: ProfileId): DataProvider {
+ val providerId = AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID
+ return getAudioTranslationContentLocale(profileId).transform(providerId) { locale ->
+ locale.getCurrentLanguage()
+ }
+ }
+
+ /**
+ * Returns a data provider for a [OppiaLocale.ContentLocale] corresponding to the user's selected
+ * language for audio voiceovers (see [getAudioTranslationContentLanguage]).
+ */
+ fun getAudioTranslationContentLocale(
+ profileId: ProfileId
+ ): DataProvider {
+ val providerId = AUDIO_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID
+ return getSystemLanguage().transformAsync(providerId) { systemLanguage ->
+ val language = computeAudioTranslationContentLanguage(profileId, systemLanguage)
+ val audioTranslationLocale = localeController.retrieveAudioTranslationsLocale(language)
+ return@transformAsync audioTranslationLocale.retrieveData()
+ }
+ }
+
+ /**
+ * Updates the language to be used by the specified user for audio voiceover selection. Note that
+ * the provided [AudioTranslationLanguageSelection] provides the user with the option of either
+ * selecting a specific supported language for audio voiceovers, or to fall back to whatever the
+ * app language selection is (which may be the system default).
+ *
+ * Note that the app guarantees a list of languages to support for audio voiceovers as a superset.
+ * The actual availability for a particular language is topic-dependent.
+ *
+ * @return a [DataProvider] which succeeds only if the update succeeds, otherwise fails (only one
+ * result is ever provided)
+ */
+ fun updateAudioTranslationContentLanguage(
+ profileId: ProfileId,
+ selection: AudioTranslationLanguageSelection
+ ): DataProvider {
+ val providerId = UPDATE_AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID
+ return dataProviders.createInMemoryDataProviderAsync(providerId) {
+ updateAudioTranslationContentLanguageSelection(profileId, selection)
+ return@createInMemoryDataProviderAsync AsyncResult.success(Unit)
+ }
+ }
+
+ /**
+ * Returns a potentially translated HTML string for the given [SubtitledHtml] to display to the
+ * user, considering the specified [WrittenTranslationContext].
+ *
+ * Generally, this method will attempt to return the translated string corresponding to this
+ * [SubtitledHtml] given the translation context, but will fall back to the default string
+ * contents if no translation exists for the subtitle.
+ */
+ fun extractString(html: SubtitledHtml, context: WrittenTranslationContext): String {
+ return context.translationsMap[html.contentId]?.html ?: html.html
+ }
+
+ /**
+ * Returns a potentially translated Unicode string for the given [SubtitledUnicode] to display to
+ * the user, with the same considerations and behavior as the [extractString] ([SubtitledHtml]
+ * variant).
+ */
+ fun extractString(unicode: SubtitledUnicode, context: WrittenTranslationContext): String {
+ return context.translationsMap[unicode.contentId]?.html ?: unicode.unicodeStr
+ }
+
+ private fun computeAppLanguage(
+ profileId: ProfileId,
+ systemLanguage: OppiaLanguage
+ ): OppiaLanguage {
+ val languageSelection = retrieveAppLanguageSelection(profileId)
+ return when (languageSelection.selectionTypeCase) {
+ AppLanguageSelection.SelectionTypeCase.SELECTED_LANGUAGE -> languageSelection.selectedLanguage
+ AppLanguageSelection.SelectionTypeCase.USE_SYSTEM_LANGUAGE_OR_APP_DEFAULT,
+ AppLanguageSelection.SelectionTypeCase.SELECTIONTYPE_NOT_SET, null -> systemLanguage
+ }
+ }
+
+ private fun computeWrittenTranslationContentLanguage(
+ profileId: ProfileId,
+ systemLanguage: OppiaLanguage
+ ): OppiaLanguage {
+ val languageSelection = retrieveWrittenTranslationContentLanguageSelection(profileId)
+ return when (languageSelection.selectionTypeCase) {
+ WrittenTranslationLanguageSelection.SelectionTypeCase.SELECTED_LANGUAGE ->
+ languageSelection.selectedLanguage
+ WrittenTranslationLanguageSelection.SelectionTypeCase.USE_APP_LANGUAGE,
+ WrittenTranslationLanguageSelection.SelectionTypeCase.SELECTIONTYPE_NOT_SET, null ->
+ computeAppLanguage(profileId, systemLanguage)
+ }
+ }
+
+ private fun computeAudioTranslationContentLanguage(
+ profileId: ProfileId,
+ systemLanguage: OppiaLanguage
+ ): OppiaLanguage {
+ val languageSelection = retrieveAudioTranslationContentLanguageSelection(profileId)
+ return when (languageSelection.selectionTypeCase) {
+ AudioTranslationLanguageSelection.SelectionTypeCase.SELECTED_LANGUAGE ->
+ languageSelection.selectedLanguage
+ AudioTranslationLanguageSelection.SelectionTypeCase.USE_APP_LANGUAGE,
+ AudioTranslationLanguageSelection.SelectionTypeCase.SELECTIONTYPE_NOT_SET, null ->
+ computeAppLanguage(profileId, systemLanguage)
+ }
+ }
+
+ private fun retrieveAppLanguageSelection(profileId: ProfileId): AppLanguageSelection {
+ return dataLock.withLock {
+ appLanguageSettings[profileId] ?: AppLanguageSelection.getDefaultInstance()
+ }
+ }
+
+ private suspend fun updateAppLanguageSelection(
+ profileId: ProfileId,
+ selection: AppLanguageSelection
+ ) {
+ dataLock.withLock {
+ appLanguageSettings[profileId] = selection
+ }
+ asyncDataSubscriptionManager.notifyChange(APP_LANGUAGE_LOCALE_DATA_PROVIDER_ID)
+ }
+
+ private fun retrieveWrittenTranslationContentLanguageSelection(
+ profileId: ProfileId
+ ): WrittenTranslationLanguageSelection {
+ return dataLock.withLock {
+ writtenTranslationLanguageSettings[profileId]
+ ?: WrittenTranslationLanguageSelection.getDefaultInstance()
+ }
+ }
+
+ private suspend fun updateWrittenTranslationContentLanguageSelection(
+ profileId: ProfileId,
+ selection: WrittenTranslationLanguageSelection
+ ) {
+ dataLock.withLock {
+ writtenTranslationLanguageSettings[profileId] = selection
+ }
+ asyncDataSubscriptionManager.notifyChange(WRITTEN_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID)
+ }
+
+ private fun retrieveAudioTranslationContentLanguageSelection(
+ profileId: ProfileId
+ ): AudioTranslationLanguageSelection {
+ return dataLock.withLock {
+ audioVoiceoverLanguageSettings[profileId]
+ ?: AudioTranslationLanguageSelection.getDefaultInstance()
+ }
+ }
+
+ private suspend fun updateAudioTranslationContentLanguageSelection(
+ profileId: ProfileId,
+ selection: AudioTranslationLanguageSelection
+ ) {
+ dataLock.withLock {
+ audioVoiceoverLanguageSettings[profileId] = selection
+ }
+ asyncDataSubscriptionManager.notifyChange(AUDIO_TRANSLATION_CONTENT_LOCALE_DATA_PROVIDER_ID)
+ }
+
+ private fun getSystemLanguage(): DataProvider =
+ localeController.retrieveSystemLanguage()
+}
diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel
index eeebbe8a1ed..677fa6b98a6 100644
--- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel
+++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel
@@ -12,7 +12,8 @@ kt_android_library(
visibility = ["//domain:__subpackages__"],
deps = [
"//third_party:javax_inject_javax_inject",
- "//utility/src/main/java/org/oppia/android/util/caching:assets",
+ "//utility/src/main/java/org/oppia/android/util/caching:annotations",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
],
)
diff --git a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt
index 738c5ea7059..461a05d32f5 100644
--- a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt
@@ -32,6 +32,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.logging.EnableConsoleLog
@@ -482,7 +483,7 @@ class AudioPlayerControllerTest {
modules = [
TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
TestDispatcherModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
- NetworkConnectionUtilDebugModule::class
+ NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent {
diff --git a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt
index 3598bfc2210..cf57e2ba696 100644
--- a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt
@@ -33,6 +33,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.data.AsyncResult
@@ -450,7 +451,7 @@ class ModifyLessonProgressControllerTest {
modules = [
TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
TestDispatcherModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
- NetworkConnectionUtilDebugModule::class
+ NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt
index c4fbb23fb35..084097b5958 100644
--- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt
@@ -59,6 +59,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.caching.TopicListToCache
@@ -73,7 +74,6 @@ import org.oppia.android.util.logging.LogLevel
import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
-import java.io.FileNotFoundException
import javax.inject.Inject
import javax.inject.Singleton
@@ -227,8 +227,8 @@ class ExplorationDataControllerTest {
verify(mockExplorationObserver).onChanged(explorationResultCaptor.capture())
assertThat(explorationResultCaptor.value.isFailure()).isTrue()
val exception = fakeExceptionLogger.getMostRecentException()
- assertThat(exception).isInstanceOf(FileNotFoundException::class.java)
- assertThat(exception).hasMessageThat().contains("NON_EXISTENT_TEST")
+ assertThat(exception).isInstanceOf(IllegalStateException::class.java)
+ assertThat(exception).hasMessageThat().contains("Asset doesn't exist: NON_EXISTENT_TEST")
}
@Test
@@ -241,8 +241,8 @@ class ExplorationDataControllerTest {
verify(mockExplorationObserver).onChanged(explorationResultCaptor.capture())
assertThat(explorationResultCaptor.value.isFailure()).isTrue()
val exception = fakeExceptionLogger.getMostRecentException()
- assertThat(exception).isInstanceOf(FileNotFoundException::class.java)
- assertThat(exception).hasMessageThat().contains("NON_EXISTENT_TEST")
+ assertThat(exception).isInstanceOf(IllegalStateException::class.java)
+ assertThat(exception).hasMessageThat().contains("Asset doesn't exist: NON_EXISTENT_TEST")
}
@Test
@@ -349,7 +349,7 @@ class ExplorationDataControllerTest {
ImageClickInputModule::class, LogStorageModule::class, TestDispatcherModule::class,
RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class,
- HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class
+ HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt
index 1f10228c6d6..e067fe285b6 100644
--- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt
@@ -77,6 +77,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.caching.TopicListToCache
@@ -91,7 +92,6 @@ import org.oppia.android.util.logging.LogLevel
import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
-import java.io.FileNotFoundException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@@ -2068,8 +2068,8 @@ class ExplorationProgressControllerTest {
)
val exception = fakeExceptionLogger.getMostRecentException()
- assertThat(exception).isInstanceOf(FileNotFoundException::class.java)
- assertThat(exception).hasMessageThat().contains(INVALID_EXPLORATION_ID)
+ assertThat(exception).isInstanceOf(IllegalStateException::class.java)
+ assertThat(exception).hasMessageThat().contains("Asset doesn't exist: $INVALID_EXPLORATION_ID")
}
@Test
@@ -3843,7 +3843,7 @@ class ExplorationProgressControllerTest {
ImageClickInputModule::class, LogStorageModule::class, TestDispatcherModule::class,
RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class,
- HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class
+ HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt
index 387975d0071..93e6ea7cbc4 100644
--- a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt
@@ -41,6 +41,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.caching.TopicListToCache
@@ -527,7 +528,7 @@ class ExplorationCheckpointControllerTest {
modules = [
TestModule::class, TestLogReportingModule::class,
TestExplorationStorageModule::class, TestDispatcherModule::class, RobolectricModule::class,
- LogStorageModule::class, NetworkConnectionUtilDebugModule::class
+ LogStorageModule::class, NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationStorageModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationStorageModuleTest.kt
index 51e98fdb3de..e195d5e2caf 100644
--- a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationStorageModuleTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationStorageModuleTest.kt
@@ -21,6 +21,7 @@ import org.oppia.android.testing.environment.TestEnvironmentConfig
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.data.DataProvidersInjector
@@ -108,7 +109,7 @@ class ExplorationStorageModuleTest {
modules = [
TestModule::class, TestLogReportingModule::class,
ExplorationStorageModule::class, TestDispatcherModule::class, RobolectricModule::class,
- LogStorageModule::class, NetworkConnectionUtilDebugModule::class
+ LogStorageModule::class, NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerDebugImplTest.kt b/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerDebugImplTest.kt
index 26a6abd5a76..1964751248c 100644
--- a/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerDebugImplTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerDebugImplTest.kt
@@ -30,6 +30,7 @@ import org.oppia.android.testing.environment.TestEnvironmentConfig
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.data.DataProvidersInjector
import org.oppia.android.util.data.DataProvidersInjectorProvider
@@ -376,7 +377,7 @@ class HintHandlerDebugImplTest {
modules = [
TestModule::class, HintsAndSolutionDebugModule::class, HintsAndSolutionConfigModule::class,
TestLogReportingModule::class, TestDispatcherModule::class, RobolectricModule::class,
- LoggerModule::class
+ LoggerModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt b/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt
index d75479d56b6..6340c2cf4eb 100644
--- a/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt
@@ -30,6 +30,7 @@ import org.oppia.android.testing.environment.TestEnvironmentConfig
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.data.DataProvidersInjector
import org.oppia.android.util.data.DataProvidersInjectorProvider
@@ -1970,7 +1971,7 @@ class HintHandlerProdImplTest {
modules = [
TestModule::class, HintsAndSolutionProdModule::class, HintsAndSolutionConfigModule::class,
TestLogReportingModule::class, TestDispatcherModule::class, RobolectricModule::class,
- LoggerModule::class
+ LoggerModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/AndroidLocaleFactoryTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/AndroidLocaleFactoryTest.kt
new file mode 100644
index 00000000000..99d29c39e61
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/locale/AndroidLocaleFactoryTest.kt
@@ -0,0 +1,1629 @@
+package org.oppia.android.domain.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.LanguageSupportDefinition.AndroidLanguageId
+import org.oppia.android.app.model.LanguageSupportDefinition.IetfBcp47LanguageId
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
+import org.oppia.android.app.model.LanguageSupportDefinition.MacaronicLanguageId
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.oppia.android.testing.assertThrows
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.locale.LocaleProdModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Tests for [AndroidLocaleFactory].
+ *
+ * Note that these tests depend on real locales being present in the local environment
+ * (Robolectric).
+ */
+// FunctionName: test names are conventionally named with underscores.
+// SameParameterValue: tests should have specific context included/excluded for readability.
+@Suppress("FunctionName", "SameParameterValue")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class AndroidLocaleFactoryTest {
+ @Inject
+ lateinit var androidLocaleFactory: AndroidLocaleFactory
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testCreateLocale_default_throwsException() {
+ val exception = assertThrows(IllegalStateException::class) {
+ androidLocaleFactory.createAndroidLocale(OppiaLocaleContext.getDefaultInstance())
+ }
+
+ // The operation should fail since there's no language type defined.
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
+ }
+
+ /* Tests for app strings. */
+
+ @Test
+ fun testCreateLocale_appStrings_withAndroidId_compatible_returnsAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ appStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withIetfId_compatible_returnsIetfLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ appStringId = createLanguageId(ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withRegionIetfId_compatible_returnsIetfLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ appStringId = createLanguageId(ietfBcp47LanguageId = PT_BR_IETF_LANGUAGE_ID),
+ regionDefinition = REGION_US
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale. Note that BR is matched since the IETF
+ // language tag includes the region.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withMacaronic_compatible_returnsMacaronicLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ appStringId = createLanguageId(macaronicLanguageId = PT_BR_MACARONIC_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_androidIdAndIetf_returnsAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = createLanguageId(
+ androidLanguageId = EN_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID,
+ ),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Android is preferred when both are present. Note no region is provided since the Android
+ // language is missing a region definition.
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_androidIdAndMacaronic_returnsAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = createLanguageId(
+ androidLanguageId = EN_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = PT_BR_MACARONIC_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Android is preferred when both are present. Note no region is provided since the Android
+ // language is missing a region definition.
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withAndroidId_incompatible_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withIetfId_incompatible_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(ietfBcp47LanguageId = QQ_ZZ_IETF_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withRegionIetfId_incompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ appStringId = createLanguageId(ietfBcp47LanguageId = PT_ZZ_IETF_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language's region doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withIetfId_incompRegion_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ appStringId = createLanguageId(ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_ZZ
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the supplied region doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withMacaronicId_incompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(macaronicLanguageId = QQ_ZZ_MACARONIC_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withMacaronicId_invalid_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(macaronicLanguageId = INVALID_MACARONIC_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language is invalid.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_withSdkIncompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ primaryMinSdkVersion = 99,
+ fallbackAppStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language isn't compatible with the current SDK.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_incompat_androidAndIetfFallback_returnsAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(
+ androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = HI_IETF_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked since Android IDs take precedence among multiple fallback options.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_incompat_androidMacaronicFallbacks_returnsAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(
+ androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = HI_IN_MACARONIC_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked since Android IDs take precedence among multiple fallback options.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_androidId_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' is the exact locale being requested.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_androidId_ietf_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(
+ androidLanguageId = QQ_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = QQ_IETF_LANGUAGE_ID
+ ),
+ fallbackAppStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' takes precedence over the IETF language since Android IDs are picked first when
+ // creating a forced locale.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_androidId_mac_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(
+ androidLanguageId = QQ_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = HI_EN_MACARONIC_LANGUAGE_ID
+ ),
+ fallbackAppStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' takes precedence over the macaronic language since Android IDs are picked first when
+ // creating a forced locale.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_ietf_allIncompat_returnsForcedIetfLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(ietfBcp47LanguageId = QQ_IETF_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The IETF language ID is used for the forced locale (note that fallback languages are ignored
+ // when computing the forced locale).
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEqualTo("IN")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_mac_allIncompat_returnsForcedMacaronicLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(macaronicLanguageId = HI_EN_MACARONIC_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Hinglish macaronic language ID is used for the forced locale (note that fallback
+ // languages are ignored when computing the forced locale).
+ assertThat(locale.language).isEqualTo("hi")
+ assertThat(locale.country).isEqualTo("EN")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_mac_allIncompat_invalidMac_throwsException() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = createLanguageId(macaronicLanguageId = INVALID_MACARONIC_LANGUAGE_ID),
+ fallbackAppStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val exception = assertThrows(IllegalStateException::class) {
+ androidLocaleFactory.createAndroidLocale(context)
+ }
+
+ assertThat(exception).hasMessageThat().contains("Invalid macaronic ID")
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_primaryAndFallbackSdkIncompat_returnsForcedLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ primaryMinSdkVersion = 99,
+ fallbackAppStringId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ fallbackMinSdkVersion = 99,
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The 'qq' language should be matched as a forced profile since both language IDs are
+ // SDK-incompatible (despite the fallback being a matchable language).
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_forMacroAndroidIdLanguage_returnsLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // Simple macro languages may not match any internal locales due to missing regions. They should
+ // still become a valid locale (due to wildcard matching internally).
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_appStrings_allIncompat_invalidLangType_throwsException() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = LanguageId.getDefaultInstance(),
+ regionDefinition = REGION_INDIA
+ )
+
+ val exception = assertThrows(IllegalStateException::class) {
+ androidLocaleFactory.createAndroidLocale(context)
+ }
+
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
+ }
+
+ /* Tests for written content strings. */
+
+ @Test
+ fun testCreateLocale_contentStrings_withAndroidId_compatible_returnsAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ contentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withIetfId_compatible_returnsIetfLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ contentStringId = createLanguageId(ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withRegionIetfId_compatible_returnsIetfLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ contentStringId = createLanguageId(ietfBcp47LanguageId = PT_BR_IETF_LANGUAGE_ID),
+ regionDefinition = REGION_US
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale. Note that BR is matched since the IETF
+ // language tag includes the region.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withMacaronic_compatible_returnsMacaronicLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ contentStringId = createLanguageId(macaronicLanguageId = PT_BR_MACARONIC_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_androidIdAndIetf_returnsAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = createLanguageId(
+ androidLanguageId = EN_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID,
+ ),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Android is preferred when both are present. Note no region is provided since the Android
+ // language is missing a region definition.
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_androidIdAndMacaronic_returnsAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = createLanguageId(
+ androidLanguageId = EN_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = PT_BR_MACARONIC_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Android is preferred when both are present. Note no region is provided since the Android
+ // language is missing a region definition.
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withAndroidId_incompatible_returnsFallbackAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withIetfId_incompatible_returnsFallbackAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(ietfBcp47LanguageId = QQ_ZZ_IETF_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withRegionIetfId_incompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ contentStringId = createLanguageId(ietfBcp47LanguageId = PT_ZZ_IETF_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language's region doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withIetfId_incompRegion_returnsFallbackAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ contentStringId = createLanguageId(ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_ZZ
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the supplied region doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withMacaronicId_incompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(macaronicLanguageId = QQ_ZZ_MACARONIC_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withMacaronicId_invalid_returnsFallbackAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(macaronicLanguageId = INVALID_MACARONIC_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language is invalid.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_withSdkIncompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ primaryMinSdkVersion = 99,
+ fallbackContentStringId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language isn't compatible with the current SDK.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_incompat_androidAndIetfFallback_returnsAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(
+ androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = HI_IETF_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked since Android IDs take precedence among multiple fallback options.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_incompat_androidMacaronicFallbacks_returnsAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(
+ androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = HI_IN_MACARONIC_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked since Android IDs take precedence among multiple fallback options.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_androidId_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' is the exact locale being requested.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_androidId_ietf_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(
+ androidLanguageId = QQ_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = QQ_IETF_LANGUAGE_ID
+ ),
+ fallbackContentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' takes precedence over the IETF language since Android IDs are picked first when
+ // creating a forced locale.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_androidId_mac_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(
+ androidLanguageId = QQ_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = HI_EN_MACARONIC_LANGUAGE_ID
+ ),
+ fallbackContentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' takes precedence over the macaronic language since Android IDs are picked first when
+ // creating a forced locale.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_ietf_allIncompat_returnsForcedIetfLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(ietfBcp47LanguageId = QQ_IETF_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The IETF language ID is used for the forced locale (note that fallback languages are ignored
+ // when computing the forced locale).
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEqualTo("IN")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_mac_allIncompat_returnsForcedMacaronicLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(macaronicLanguageId = HI_EN_MACARONIC_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Hinglish macaronic language ID is used for the forced locale (note that fallback
+ // languages are ignored when computing the forced locale).
+ assertThat(locale.language).isEqualTo("hi")
+ assertThat(locale.country).isEqualTo("EN")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_mac_allIncompat_invalidMac_throwsException() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = createLanguageId(macaronicLanguageId = INVALID_MACARONIC_LANGUAGE_ID),
+ fallbackContentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val exception = assertThrows(IllegalStateException::class) {
+ androidLocaleFactory.createAndroidLocale(context)
+ }
+
+ assertThat(exception).hasMessageThat().contains("Invalid macaronic ID")
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_primaryAndFallbackSdkIncompat_returnsForcedLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ contentStringId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ primaryMinSdkVersion = 99,
+ fallbackContentStringId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ fallbackMinSdkVersion = 99,
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The 'qq' language should be matched as a forced profile since both language IDs are
+ // SDK-incompatible (despite the fallback being a matchable language).
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_forMacroAndroidIdLanguage_returnsLocale() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // Simple macro languages may not match any internal locales due to missing regions. They should
+ // still become a valid locale (due to wildcard matching internally).
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_contentStrings_allIncompat_invalidLangType_throwsException() {
+ val context =
+ createContentStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = LanguageId.getDefaultInstance(),
+ regionDefinition = REGION_INDIA
+ )
+
+ val exception = assertThrows(IllegalStateException::class) {
+ androidLocaleFactory.createAndroidLocale(context)
+ }
+
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
+ }
+
+ /* Tests for audio translations. */
+
+ @Test
+ fun testCreateLocale_audioSubs_withAndroidId_compatible_returnsAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ audioTranslationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withIetfId_compatible_returnsIetfLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ audioTranslationId = createLanguageId(ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withRegionIetfId_compatible_returnsIetfLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ audioTranslationId = createLanguageId(ietfBcp47LanguageId = PT_BR_IETF_LANGUAGE_ID),
+ regionDefinition = REGION_US
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale. Note that BR is matched since the IETF
+ // language tag includes the region.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withMacaronic_compatible_returnsMacaronicLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ audioTranslationId = createLanguageId(macaronicLanguageId = PT_BR_MACARONIC_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_androidIdAndIetf_returnsAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.ENGLISH,
+ audioTranslationId = createLanguageId(
+ androidLanguageId = EN_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID,
+ ),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Android is preferred when both are present. Note no region is provided since the Android
+ // language is missing a region definition.
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_androidIdAndMacaronic_returnsAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.ENGLISH,
+ audioTranslationId = createLanguageId(
+ androidLanguageId = EN_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = PT_BR_MACARONIC_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Android is preferred when both are present. Note no region is provided since the Android
+ // language is missing a region definition.
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withAndroidId_incompatible_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withIetfId_incompatible_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(ietfBcp47LanguageId = QQ_ZZ_IETF_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withRegionIetfId_incompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ audioXlationId = createLanguageId(ietfBcp47LanguageId = PT_ZZ_IETF_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language's region doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withIetfId_incompRegion_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.BRAZILIAN_PORTUGUESE,
+ audioXlationId = createLanguageId(ietfBcp47LanguageId = PT_IETF_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_ZZ
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the supplied region doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withMacaronicId_incompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(macaronicLanguageId = QQ_ZZ_MACARONIC_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language doesn't match a real locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withMacaronicId_invalid_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(macaronicLanguageId = INVALID_MACARONIC_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language is invalid.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_withSdkIncompat_returnsFallbackAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.ENGLISH,
+ audioXlationId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ primaryMinSdkVersion = 99,
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_BRAZIL
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked because the primary language isn't compatible with the current SDK.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_incompat_androidAndIetfFallback_returnsAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(
+ androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = HI_IETF_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked since Android IDs take precedence among multiple fallback options.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_incompat_androidMacaronicFallbacks_returnsAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(
+ androidLanguageId = PT_BR_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = HI_IN_MACARONIC_LANGUAGE_ID
+ ),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // pt-BR should be picked since Android IDs take precedence among multiple fallback options.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_androidId_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' is the exact locale being requested.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_androidId_ietf_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(
+ androidLanguageId = QQ_ANDROID_LANGUAGE_ID,
+ ietfBcp47LanguageId = QQ_IETF_LANGUAGE_ID
+ ),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' takes precedence over the IETF language since Android IDs are picked first when
+ // creating a forced locale.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_androidId_mac_allIncompat_returnsForcedAndroidIdLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(
+ androidLanguageId = QQ_ANDROID_LANGUAGE_ID,
+ macaronicLanguageId = HI_EN_MACARONIC_LANGUAGE_ID
+ ),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // 'qq' takes precedence over the macaronic language since Android IDs are picked first when
+ // creating a forced locale.
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_ietf_allIncompat_returnsForcedIetfLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(ietfBcp47LanguageId = QQ_IETF_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The IETF language ID is used for the forced locale (note that fallback languages are ignored
+ // when computing the forced locale).
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEqualTo("IN")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_mac_allIncompat_returnsForcedMacaronicLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(macaronicLanguageId = HI_EN_MACARONIC_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The Hinglish macaronic language ID is used for the forced locale (note that fallback
+ // languages are ignored when computing the forced locale).
+ assertThat(locale.language).isEqualTo("hi")
+ assertThat(locale.country).isEqualTo("EN")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_mac_allIncompat_invalidMac_throwsException() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.ENGLISH,
+ audioXlationId = createLanguageId(macaronicLanguageId = INVALID_MACARONIC_LANGUAGE_ID),
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val exception = assertThrows(IllegalStateException::class) {
+ androidLocaleFactory.createAndroidLocale(context)
+ }
+
+ assertThat(exception).hasMessageThat().contains("Invalid macaronic ID")
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_primaryAndFallbackSdkIncompat_returnsForcedLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ audioXlationId = createLanguageId(androidLanguageId = QQ_ANDROID_LANGUAGE_ID),
+ primaryMinSdkVersion = 99,
+ fallbackAudioXlationId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ fallbackMinSdkVersion = 99,
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // The 'qq' language should be matched as a forced profile since both language IDs are
+ // SDK-incompatible (despite the fallback being a matchable language).
+ assertThat(locale.language).isEqualTo("qq")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_forMacroAndroidIdLanguage_returnsLocale() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.ENGLISH,
+ audioTranslationId = createLanguageId(androidLanguageId = EN_ANDROID_LANGUAGE_ID),
+ regionDefinition = REGION_INDIA
+ )
+
+ val locale = androidLocaleFactory.createAndroidLocale(context)
+
+ // Simple macro languages may not match any internal locales due to missing regions. They should
+ // still become a valid locale (due to wildcard matching internally).
+ assertThat(locale.language).isEqualTo("en")
+ assertThat(locale.country).isEmpty()
+ }
+
+ @Test
+ fun testCreateLocale_audioSubs_allIncompat_invalidLangType_throwsException() {
+ val context =
+ createAudioTranslationContext(
+ language = OppiaLanguage.ENGLISH,
+ audioTranslationId = LanguageId.getDefaultInstance(),
+ regionDefinition = REGION_INDIA
+ )
+
+ val exception = assertThrows(IllegalStateException::class) {
+ androidLocaleFactory.createAndroidLocale(context)
+ }
+
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
+ }
+
+ private fun createLanguageId(androidLanguageId: AndroidLanguageId): LanguageId {
+ return LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = androidLanguageId
+ }.build()
+ }
+
+ private fun createLanguageId(ietfBcp47LanguageId: IetfBcp47LanguageId): LanguageId {
+ return LanguageId.newBuilder().apply {
+ ietfBcp47Id = ietfBcp47LanguageId
+ }.build()
+ }
+
+ private fun createLanguageId(
+ androidLanguageId: AndroidLanguageId,
+ ietfBcp47LanguageId: IetfBcp47LanguageId
+ ): LanguageId {
+ return createLanguageId(androidLanguageId).toBuilder()
+ .mergeFrom(createLanguageId(ietfBcp47LanguageId))
+ .build()
+ }
+
+ private fun createLanguageId(macaronicLanguageId: MacaronicLanguageId): LanguageId {
+ return LanguageId.newBuilder().apply {
+ macaronicId = macaronicLanguageId
+ }.build()
+ }
+
+ private fun createLanguageId(
+ androidLanguageId: AndroidLanguageId,
+ macaronicLanguageId: MacaronicLanguageId
+ ): LanguageId {
+ return createLanguageId(androidLanguageId).toBuilder()
+ .mergeFrom(createLanguageId(macaronicLanguageId))
+ .build()
+ }
+
+ private fun createAppStringsContext(
+ language: OppiaLanguage,
+ appStringId: LanguageId,
+ minSdkVersion: Int = 1,
+ regionDefinition: RegionSupportDefinition
+ ): OppiaLocaleContext {
+ return OppiaLocaleContext.newBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ minAndroidSdkVersion = minSdkVersion
+ this.appStringId = appStringId
+ }.build()
+ this.regionDefinition = regionDefinition
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ }.build()
+ }
+
+ private fun createAppStringsContext(
+ language: OppiaLanguage,
+ appStringId: LanguageId,
+ primaryMinSdkVersion: Int = 1,
+ fallbackAppStringId: LanguageId,
+ fallbackMinSdkVersion: Int = 1,
+ regionDefinition: RegionSupportDefinition
+ ): OppiaLocaleContext {
+ return createAppStringsContext(
+ language, appStringId, primaryMinSdkVersion, regionDefinition
+ ).toBuilder().apply {
+ fallbackLanguageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ minAndroidSdkVersion = fallbackMinSdkVersion
+ this.appStringId = fallbackAppStringId
+ }.build()
+ }.build()
+ }
+
+ private fun createContentStringsContext(
+ language: OppiaLanguage,
+ contentStringId: LanguageId,
+ minSdkVersion: Int = 1,
+ regionDefinition: RegionSupportDefinition
+ ): OppiaLocaleContext {
+ return OppiaLocaleContext.newBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ minAndroidSdkVersion = minSdkVersion
+ this.contentStringId = contentStringId
+ }.build()
+ this.regionDefinition = regionDefinition
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+ }.build()
+ }
+
+ private fun createContentStringsContext(
+ language: OppiaLanguage,
+ contentStringId: LanguageId,
+ primaryMinSdkVersion: Int = 1,
+ fallbackContentStringId: LanguageId,
+ fallbackMinSdkVersion: Int = 1,
+ regionDefinition: RegionSupportDefinition
+ ): OppiaLocaleContext {
+ return createContentStringsContext(
+ language, contentStringId, primaryMinSdkVersion, regionDefinition
+ ).toBuilder().apply {
+ fallbackLanguageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ minAndroidSdkVersion = fallbackMinSdkVersion
+ this.contentStringId = fallbackContentStringId
+ }.build()
+ }.build()
+ }
+
+ private fun createAudioTranslationContext(
+ language: OppiaLanguage,
+ audioTranslationId: LanguageId,
+ minSdkVersion: Int = 1,
+ regionDefinition: RegionSupportDefinition
+ ): OppiaLocaleContext {
+ return OppiaLocaleContext.newBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ minAndroidSdkVersion = minSdkVersion
+ this.audioTranslationId = audioTranslationId
+ }.build()
+ this.regionDefinition = regionDefinition
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+ }.build()
+ }
+
+ private fun createAudioTranslationContext(
+ language: OppiaLanguage,
+ audioXlationId: LanguageId,
+ primaryMinSdkVersion: Int = 1,
+ fallbackAudioXlationId: LanguageId,
+ fallbackMinSdkVersion: Int = 1,
+ regionDefinition: RegionSupportDefinition
+ ): OppiaLocaleContext {
+ return createAudioTranslationContext(
+ language, audioXlationId, primaryMinSdkVersion, regionDefinition
+ ).toBuilder().apply {
+ fallbackLanguageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ minAndroidSdkVersion = fallbackMinSdkVersion
+ this.audioTranslationId = fallbackAudioXlationId
+ }.build()
+ }.build()
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerAndroidLocaleFactoryTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(androidLocaleFactoryTest: AndroidLocaleFactoryTest)
+ }
+
+ private companion object {
+ private val REGION_BRAZIL = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.BRAZIL
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "BR"
+ }.build()
+ }.build()
+
+ private val REGION_US = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.UNITED_STATES
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "US"
+ }.build()
+ }.build()
+
+ private val REGION_INDIA = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.INDIA
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "IN"
+ }.build()
+ }.build()
+
+ private val REGION_ZZ = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.REGION_UNSPECIFIED
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "ZZ"
+ }.build()
+ }.build()
+
+ private val EN_ANDROID_LANGUAGE_ID = AndroidLanguageId.newBuilder().apply {
+ languageCode = "en"
+ }.build()
+
+ private val PT_BR_ANDROID_LANGUAGE_ID = AndroidLanguageId.newBuilder().apply {
+ languageCode = "pt"
+ regionCode = "BR"
+ }.build()
+
+ private val QQ_ANDROID_LANGUAGE_ID = AndroidLanguageId.newBuilder().apply {
+ languageCode = "qq"
+ }.build()
+
+ private val PT_IETF_LANGUAGE_ID = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt"
+ }.build()
+
+ private val HI_IETF_LANGUAGE_ID = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "hi"
+ }.build()
+
+ private val PT_BR_IETF_LANGUAGE_ID = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt-BR"
+ }.build()
+
+ // Has a valid language, but unsupported region.
+ private val PT_ZZ_IETF_LANGUAGE_ID = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt-ZZ"
+ }.build()
+
+ private val QQ_IETF_LANGUAGE_ID = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "qq"
+ }.build()
+
+ private val QQ_ZZ_IETF_LANGUAGE_ID = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "qq-ZZ"
+ }.build()
+
+ private val PT_BR_MACARONIC_LANGUAGE_ID = MacaronicLanguageId.newBuilder().apply {
+ // This is a loose definition for macaronic language that's being done to test compatible
+ // cases (though in reality macaronic languages aren't expected to ever match with system
+ // locales).
+ combinedLanguageCode = "pt-br"
+ }.build()
+
+ private val HI_IN_MACARONIC_LANGUAGE_ID = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-IN"
+ }.build()
+
+ private val HI_EN_MACARONIC_LANGUAGE_ID = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-EN"
+ }.build()
+
+ private val QQ_ZZ_MACARONIC_LANGUAGE_ID = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "qq-zz"
+ }.build()
+
+ private val INVALID_MACARONIC_LANGUAGE_ID = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "languagewithoutregion"
+ }.build()
+ }
+}
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel
new file mode 100644
index 00000000000..c64d6948ba6
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel
@@ -0,0 +1,141 @@
+"""
+Tests for language & locale domain components.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "AndroidLocaleFactoryTest",
+ srcs = ["AndroidLocaleFactoryTest.kt"],
+ custom_package = "org.oppia.android.domain.locale",
+ test_class = "org.oppia.android.domain.locale.AndroidLocaleFactoryTest",
+ test_manifest = "//domain:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain/src/main/java/org/oppia/android/domain/locale:android_locale_factory",
+ "//testing",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ ],
+)
+
+oppia_android_test(
+ name = "ContentLocaleImplTest",
+ srcs = ["ContentLocaleImplTest.kt"],
+ custom_package = "org.oppia.android.domain.locale",
+ test_class = "org.oppia.android.domain.locale.ContentLocaleImplTest",
+ test_manifest = "//domain:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain/src/main/java/org/oppia/android/domain/locale:content_locale_impl",
+ "//model:languages_java_proto_lite",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_extensions_truth-liteproto-extension",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ ],
+)
+
+oppia_android_test(
+ name = "DisplayLocaleImplTest",
+ srcs = ["DisplayLocaleImplTest.kt"],
+ custom_package = "org.oppia.android.domain.locale",
+ test_class = "org.oppia.android.domain.locale.DisplayLocaleImplTest",
+ test_manifest = "//domain:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain:test_resources",
+ "//domain/src/main/java/org/oppia/android/domain/locale:display_locale_impl",
+ "//model:languages_java_proto_lite",
+ "//testing",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_extensions_truth-liteproto-extension",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale/testing:test_module",
+ ],
+)
+
+oppia_android_test(
+ name = "LanguageConfigRetrieverTest",
+ srcs = ["LanguageConfigRetrieverTest.kt"],
+ custom_package = "org.oppia.android.domain.locale",
+ test_class = "org.oppia.android.domain.locale.LanguageConfigRetrieverTest",
+ test_manifest = "//domain:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain/src/main/java/org/oppia/android/domain/locale:language_config_retriever",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_extensions_truth-liteproto-extension",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
+ "//utility/src/main/java/org/oppia/android/util/logging:prod_module",
+ ],
+)
+
+oppia_android_test(
+ name = "LanguageConfigRetrieverWithoutAssetsTest",
+ srcs = ["LanguageConfigRetrieverWithoutAssetsTest.kt"],
+ custom_package = "org.oppia.android.domain.locale",
+ test_class = "org.oppia.android.domain.locale.LanguageConfigRetrieverWithoutAssetsTest",
+ test_manifest = "//domain:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain/src/main/java/org/oppia/android/domain/locale:language_config_retriever",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_extensions_truth-liteproto-extension",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching/testing:asset_test_no_op_module",
+ "//utility/src/main/java/org/oppia/android/util/logging:prod_module",
+ ],
+)
+
+oppia_android_test(
+ name = "LocaleControllerTest",
+ srcs = ["LocaleControllerTest.kt"],
+ custom_package = "org.oppia.android.domain.locale",
+ test_class = "org.oppia.android.domain.locale.LocaleControllerTest",
+ test_manifest = "//domain:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller",
+ "//model:languages_java_proto_lite",
+ "//testing",
+ "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_extensions_truth-liteproto-extension",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_mockito_mockito-core",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ "//utility/src/main/java/org/oppia/android/util/networking:debug_module",
+ ],
+)
+
+dagger_rules()
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/ContentLocaleImplTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/ContentLocaleImplTest.kt
new file mode 100644
index 00000000000..ad9ad675268
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/locale/ContentLocaleImplTest.kt
@@ -0,0 +1,174 @@
+package org.oppia.android.domain.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Singleton
+
+/**
+ * Tests for [ContentLocaleImpl].
+ *
+ * Note that the tests for verifying correct toString() & equals() were verified with a version of
+ * the implementation which didn't automatically implement them via a data class to verify that they
+ * fail as expected.
+ */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class ContentLocaleImplTest {
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testCreateContentLocaleImpl_defaultInstance_hasDefaultInstanceContext() {
+ val impl = ContentLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ assertThat(impl.oppiaLocaleContext).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testCreateContentLocaleImpl_forProvidedContext_hasCorrectInstanceContext() {
+ val impl = ContentLocaleImpl(LOCALE_CONTEXT)
+
+ assertThat(impl.oppiaLocaleContext).isEqualTo(LOCALE_CONTEXT)
+ }
+
+ @Test
+ fun testToString_returnsNonDefaultString() {
+ val impl = ContentLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ val str = impl.toString()
+
+ // Verify that the string includes some details about the implementation (this is a potentially
+ // fragile test).
+ assertThat(str).contains("OppiaLocaleContext")
+ }
+
+ @Test
+ fun testEquals_withNullValue_returnsFalse() {
+ val impl = ContentLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ assertThat(impl).isNotEqualTo(null)
+ }
+
+ @Test
+ fun testEquals_withSameObject_returnsTrue() {
+ val impl = ContentLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ assertThat(impl).isEqualTo(impl)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithDifferentContexts_returnsFalse() {
+ val impl1 = ContentLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl2 = ContentLocaleImpl(LOCALE_CONTEXT)
+
+ assertThat(impl1).isNotEqualTo(impl2)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithDifferentContexts_reversed_returnsFalse() {
+ val impl1 = ContentLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl2 = ContentLocaleImpl(LOCALE_CONTEXT)
+
+ assertThat(impl2).isNotEqualTo(impl1)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithSameContexts_returnsTrue() {
+ val impl1 = ContentLocaleImpl(LOCALE_CONTEXT)
+ // Create a copy of the proto, too.
+ val impl2 = ContentLocaleImpl(LOCALE_CONTEXT.toBuilder().build())
+
+ // This is somewhat testing the implementation of data classes, but it's important to verify
+ // that the implementation correctly satisfies the contract outlined in OppiaLocale.
+ assertThat(impl1).isEqualTo(impl2)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithSameContexts_reversed_returnsTrue() {
+ val impl1 = ContentLocaleImpl(LOCALE_CONTEXT)
+ val impl2 = ContentLocaleImpl(LOCALE_CONTEXT.toBuilder().build())
+
+ assertThat(impl2).isEqualTo(impl1)
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerContentLocaleImplTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(contentLocaleImplTest: ContentLocaleImplTest)
+ }
+
+ private companion object {
+ private val LOCALE_CONTEXT = OppiaLocaleContext.newBuilder().apply {
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.HINGLISH
+ fallbackMacroLanguage = OppiaLanguage.ENGLISH
+ minAndroidSdkVersion = 1
+ contentStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
+ macaronicId = LanguageSupportDefinition.MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-en"
+ }.build()
+ }.build()
+ }.build()
+ regionDefinition = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.INDIA
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "IN"
+ }.build()
+ }.build()
+ }.build()
+ }
+}
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt
new file mode 100644
index 00000000000..0ff73fdb1b8
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt
@@ -0,0 +1,658 @@
+package org.oppia.android.domain.locale
+
+import android.app.Application
+import android.content.Context
+import android.content.res.Resources
+import androidx.core.view.ViewCompat
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.oppia.android.domain.R
+import org.oppia.android.testing.assertThrows
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.locale.OppiaBidiFormatter
+import org.oppia.android.util.locale.OppiaLocale
+import org.oppia.android.util.locale.testing.LocaleTestModule
+import org.oppia.android.util.locale.testing.TestOppiaBidiFormatter
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [DisplayLocaleImpl]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class DisplayLocaleImplTest {
+ @Inject
+ lateinit var machineLocale: OppiaLocale.MachineLocale
+
+ @Inject
+ lateinit var androidLocaleFactory: AndroidLocaleFactory
+
+ @Inject
+ lateinit var formatterFactory: OppiaBidiFormatter.Factory
+
+ @Inject
+ lateinit var wrapperChecker: TestOppiaBidiFormatter.Checker
+
+ @Inject
+ lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testCreateDisplayLocaleImpl_defaultInstance_hasDefaultInstanceContext() {
+ val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ assertThat(impl.localeContext).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testCreateDisplayLocaleImpl_forProvidedContext_hasCorrectInstanceContext() {
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
+
+ assertThat(impl.localeContext).isEqualTo(EGYPT_ARABIC_CONTEXT)
+ }
+
+ @Test
+ fun testToString_returnsNonDefaultString() {
+ val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ val str = impl.toString()
+
+ // Verify that the string includes some details about the implementation (this is a potentially
+ // fragile test).
+ assertThat(str).contains("OppiaLocaleContext")
+ }
+
+ @Test
+ fun testEquals_withNullValue_returnsFalse() {
+ val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ assertThat(impl).isNotEqualTo(null)
+ }
+
+ @Test
+ fun testEquals_withSameObject_returnsTrue() {
+ val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+
+ assertThat(impl).isEqualTo(impl)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithDifferentContexts_returnsFalse() {
+ val impl1 = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
+ val impl2 = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ assertThat(impl1).isNotEqualTo(impl2)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithDifferentContexts_reversed_returnsFalse() {
+ val impl1 = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
+ val impl2 = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ assertThat(impl2).isNotEqualTo(impl1)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithSameContexts_returnsTrue() {
+ val impl1 = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
+ // Create a copy of the proto, too.
+ val impl2 = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT.toBuilder().build())
+
+ // This is somewhat testing the implementation of data classes, but it's important to verify
+ // that the implementation correctly satisfies the contract outlined in OppiaLocale.
+ assertThat(impl1).isEqualTo(impl2)
+ }
+
+ @Test
+ fun testEquals_twoDifferentInstancesWithSameContexts_reversed_returnsTrue() {
+ val impl1 = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
+ val impl2 = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT.toBuilder().build())
+
+ assertThat(impl2).isEqualTo(impl1)
+ }
+
+ @Test
+ fun testComputeDateString_forFixedTime_returnMonthDayYearParts() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val dateString = impl.computeDateString(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ assertThat(dateString.extractNumbers()).containsExactly("24", "2019")
+ assertThat(dateString).contains("Apr")
+ }
+
+ @Test
+ fun testComputeTimeString_forFixedTime_returnMinuteHourParts() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val timeString = impl.computeTimeString(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ assertThat(timeString.extractNumbers()).containsExactly("22", "8")
+ }
+
+ @Test
+ fun testComputeDateTimeString_forFixedTime_returnsMinHourMonthDayYearParts() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val dateTimeString = impl.computeDateTimeString(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ assertThat(dateTimeString.extractNumbers()).containsExactly("22", "8", "24", "2019")
+ assertThat(dateTimeString).contains("Apr")
+ }
+
+ @Test
+ fun testGetLayoutDirection_englishContext_returnsLeftToRight() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val layoutDirection = impl.getLayoutDirection()
+
+ assertThat(layoutDirection).isEqualTo(ViewCompat.LAYOUT_DIRECTION_LTR)
+ }
+
+ @Test
+ fun testGetLayoutDirection_arabicContext_returnsRightToLeft() {
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
+
+ val layoutDirection = impl.getLayoutDirection()
+
+ assertThat(layoutDirection).isEqualTo(ViewCompat.LAYOUT_DIRECTION_RTL)
+ }
+
+ @Test
+ fun testFormatInLocaleWithWrapping_formatStringWithArgs_returnsCorrectlyFormattedString() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val formatted = impl.run { "Test with %s and %s".formatInLocaleWithWrapping("string", "11") }
+
+ assertThat(formatted).isEqualTo("Test with string and 11")
+ }
+
+ @Test
+ fun testFormatInLocaleWithWrapping_properlyWrapsArguments() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ impl.run { "Test with %s and %s".formatInLocaleWithWrapping("string", "11") }
+
+ // Verify that both arguments were wrapped.
+ assertThat(wrapperChecker.getAllWrappedUnicodeTexts()).containsExactly("string", "11")
+ }
+
+ @Test
+ fun testFormatInLocaleWithoutWrapping_formatStringWithArgs_returnsCorrectlyFormattedString() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val formatted = impl.run { "Test with %s and %s".formatInLocaleWithoutWrapping("string", "11") }
+
+ assertThat(formatted).isEqualTo("Test with string and 11")
+ }
+
+ @Test
+ fun testFormatInLocaleWithoutWrapping_doesNotWrapArguments() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ impl.run { "Test with %s and %s".formatInLocaleWithoutWrapping("string", "11") }
+
+ // Verify that none of the arguments were wrapped.
+ assertThat(wrapperChecker.getAllWrappedUnicodeTexts()).isEmpty()
+ }
+
+ @Test
+ fun testCapitalizeForHumans_capitalizedString_returnsSameString() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val capitalized = impl.run { "Title String".capitalizeForHumans() }
+
+ assertThat(capitalized).isEqualTo("Title String")
+ }
+
+ @Test
+ fun testCapitalizeForHumans_uncapitalizedString_returnsCapitalized() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val capitalized = impl.run { "lowercased string".capitalizeForHumans() }
+
+ assertThat(capitalized).isEqualTo("Lowercased string")
+ }
+
+ @Test
+ fun testCapitalizeForHumans_englishLocale_localeSensitiveCharAtStart_returnsConvertedCase() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+
+ val capitalized = impl.run { "igloo".capitalizeForHumans() }
+
+ assertThat(capitalized).isEqualTo("Igloo")
+ }
+
+ @Test
+ fun testCapitalizeForHumans_turkishLocale_localeSensitiveCharAtStart_returnsIncorrectCase() {
+ val impl = createDisplayLocaleImpl(TURKEY_TURKISH_CONTEXT)
+
+ val capitalized = impl.run { "igloo".capitalizeForHumans() }
+
+ // Note that the starting letter differs when being capitalized with a Turkish context (as
+ // compared with the English version of this test). See https://stackoverflow.com/a/11063161 for
+ // context on how casing behaviors differ based on Locales in Java.
+ assertThat(capitalized).isEqualTo("İgloo")
+ }
+
+ @Test
+ fun testGetStringInLocale_validId_returnsResourceStringForId() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run { resources.getStringInLocale(R.string.test_basic_string) }
+
+ assertThat(str).isEqualTo("Basic string")
+ }
+
+ @Test
+ fun testGetStringInLocale_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getStringInLocale(-1) }
+ }
+ }
+
+ @Test
+ fun testGetStringInLocaleWithWrapping_formatStringResourceWithArgs_returnsFormattedString() {
+ val impl = createDisplayLocaleImpl(HEBREW_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getStringInLocaleWithWrapping(
+ R.string.test_string_with_arg_hebrew, "123 Some Street, Mountain View, CA"
+ )
+ }
+
+ // This is based on the example here:
+ // https://developer.android.com/training/basics/supporting-devices/languages#FormatTextExplanationSolution.
+ assertThat(str)
+ .isEqualTo("האם התכוונת ל \u200F\u202A123 Some Street, Mountain View, CA\u202C\u200F")
+ }
+
+ @Test
+ fun testGetStringInLocaleWithWrapping_properlyWrapsArguments() {
+ val impl = createDisplayLocaleImpl(HEBREW_CONTEXT)
+ val resources = context.resources
+
+ impl.run {
+ resources.getStringInLocaleWithWrapping(
+ R.string.test_string_with_arg_hebrew, "123 Some Street, Mountain View, CA"
+ )
+ }
+
+ // Verify that the argument was wrapped.
+ assertThat(wrapperChecker.getAllWrappedUnicodeTexts())
+ .containsExactly("123 Some Street, Mountain View, CA")
+ }
+
+ @Test
+ fun testGetStringInLocaleWithWrapping_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getStringInLocaleWithWrapping(-1) }
+ }
+ }
+
+ @Test
+ fun testGetStringInLocaleWithoutWrapping_formatStringResourceWithArgs_returnsFormattedString() {
+ val impl = createDisplayLocaleImpl(HEBREW_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getStringInLocaleWithoutWrapping(
+ R.string.test_string_with_arg_hebrew, "123 Some Street, Mountain View, CA"
+ )
+ }
+
+ // This is based on the example here:
+ // https://developer.android.com/training/basics/supporting-devices/languages#FormatTextExplanationSolution.
+ // Note that the string is formatted, but due to no bidirectional wrapping the address ends up
+ // incorrectly formatted.
+ assertThat(str).isEqualTo("האם התכוונת ל 123 Some Street, Mountain View, CA")
+ }
+
+ @Test
+ fun testGetStringInLocaleWithoutWrapping_doesNotWrapArguments() {
+ val impl = createDisplayLocaleImpl(HEBREW_CONTEXT)
+ val resources = context.resources
+
+ impl.run {
+ resources.getStringInLocaleWithoutWrapping(
+ R.string.test_string_with_arg_hebrew, "123 Some Street, Mountain View, CA"
+ )
+ }
+
+ // Verify that no arguments were wrapped.
+ assertThat(wrapperChecker.getAllWrappedUnicodeTexts()).isEmpty()
+ }
+
+ @Test
+ fun testGetStringInLocaleWithoutWrapping_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getStringInLocaleWithoutWrapping(-1) }
+ }
+ }
+
+ @Test
+ fun testGetStringArrayInLocale_validId_returnsArrayAsStringList() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val strList = impl.run { resources.getStringArrayInLocale(R.array.test_str_array) }
+
+ assertThat(strList).containsExactly("Basic string", "Basic string2").inOrder()
+ }
+
+ @Test
+ fun testGetStringArrayInLocale_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getStringArrayInLocale(-1) }
+ }
+ }
+
+ @Test
+ fun testGetQuantityStringInLocale_validId_oneItem_returnsQuantityStringForSingleItem() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getQuantityStringInLocale(R.plurals.test_plural_string_no_args, 1)
+ }
+
+ assertThat(str).isEqualTo("1 item")
+ }
+
+ @Test
+ fun testGetQuantityStringInLocale_validId_twoItems_returnsQuantityStringForMultipleItems() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getQuantityStringInLocale(R.plurals.test_plural_string_no_args, 2)
+ }
+
+ // Note that the 'other' case covers most scenarios in English (per
+ // https://issuetracker.google.com/issues/36917255).
+ assertThat(str).isEqualTo("2 items")
+ }
+
+ @Test
+ fun testGetQuantityStringInLocale_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getQuantityStringInLocale(-1, 0) }
+ }
+ }
+
+ @Test
+ fun testGetQuantityStringInLocaleWithWrapping_formatStrResourceWithArgs_returnsFormattedStr() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getQuantityStringInLocaleWithWrapping(
+ R.plurals.test_plural_string_with_args, 2, "Two"
+ )
+ }
+
+ assertThat(str).isEqualTo("Two items")
+ }
+
+ @Test
+ fun testGetQuantityStringInLocaleWithWrapping_properlyWrapsArguments() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ impl.run {
+ resources.getQuantityStringInLocaleWithWrapping(
+ R.plurals.test_plural_string_with_args, 2, "Two"
+ )
+ }
+
+ // Verify that the argument was wrapped.
+ assertThat(wrapperChecker.getAllWrappedUnicodeTexts()).containsExactly("Two")
+ }
+
+ @Test
+ fun testGetQuantityStringInLocaleWithWrapping_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getQuantityStringInLocaleWithWrapping(-1, 0) }
+ }
+ }
+
+ @Test
+ fun testGetQuantityStringInLocaleWithoutWrapping_formatStrResourceWithArgs_returnsFormattedStr() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getQuantityStringInLocaleWithoutWrapping(
+ R.plurals.test_plural_string_with_args, 2, "Two"
+ )
+ }
+
+ assertThat(str).isEqualTo("Two items")
+ }
+
+ @Test
+ fun testGetQuantityStringInLocaleWithoutWrapping_doesNotWrapArguments() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ impl.run {
+ resources.getQuantityStringInLocaleWithoutWrapping(
+ R.plurals.test_plural_string_with_args, 2, "Two"
+ )
+ }
+
+ // Verify that no arguments were wrapped.
+ assertThat(wrapperChecker.getAllWrappedUnicodeTexts()).isEmpty()
+ }
+
+ @Test
+ fun testGetQuantityStringInLocaleWithoutWrapping_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getQuantityStringInLocaleWithoutWrapping(-1, 0) }
+ }
+ }
+
+ @Test
+ fun testGetQuantityTextInLocale_validId_oneItem_returnsQuantityStringForSingleItem() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getQuantityTextInLocale(R.plurals.test_plural_string_no_args, 1)
+ }
+
+ assertThat(str).isEqualTo("1 item")
+ }
+
+ @Test
+ fun testGetQuantityTextInLocale_validId_twoItems_returnsQuantityStringForMultipleItems() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ val str = impl.run {
+ resources.getQuantityTextInLocale(R.plurals.test_plural_string_no_args, 2)
+ }
+
+ // Note that the 'other' case covers most scenarios in English (per
+ // https://issuetracker.google.com/issues/36917255).
+ assertThat(str).isEqualTo("2 items")
+ }
+
+ @Test
+ fun testGetQuantityTextInLocale_nonExistentId_throwsException() {
+ val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)
+ val resources = context.resources
+
+ assertThrows(Resources.NotFoundException::class) {
+ impl.run { resources.getQuantityTextInLocale(-1, 0) }
+ }
+ }
+
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
+ DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+
+ private fun setUpTestApplicationComponent() {
+ DaggerDisplayLocaleImplTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleTestModule::class, FakeOppiaClockModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(displayLocaleImplTest: DisplayLocaleImplTest)
+ }
+
+ private companion object {
+ // Date & time: Wed Apr 24 2019 08:22:03 GMT.
+ private const val MORNING_UTC_TIMESTAMP_MILLIS = 1556094123000
+
+ private val US_ENGLISH_CONTEXT = OppiaLocaleContext.newBuilder().apply {
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.ENGLISH
+ minAndroidSdkVersion = 1
+ appStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "en"
+ }.build()
+ }.build()
+ }.build()
+ regionDefinition = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.UNITED_STATES
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "US"
+ }.build()
+ }.build()
+ }.build()
+
+ private val EGYPT_ARABIC_CONTEXT = OppiaLocaleContext.newBuilder().apply {
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.ARABIC
+ minAndroidSdkVersion = 1
+ appStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "ar"
+ }.build()
+ }.build()
+ }.build()
+ regionDefinition = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.REGION_UNSPECIFIED
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "EG"
+ }.build()
+ }.build()
+ }.build()
+
+ private val TURKEY_TURKISH_CONTEXT = OppiaLocaleContext.newBuilder().apply {
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED
+ minAndroidSdkVersion = 1
+ appStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "tr"
+ }.build()
+ }.build()
+ }.build()
+ regionDefinition = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.REGION_UNSPECIFIED
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "TR"
+ }.build()
+ }.build()
+ }.build()
+
+ private val HEBREW_CONTEXT = OppiaLocaleContext.newBuilder().apply {
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.ARABIC
+ minAndroidSdkVersion = 1
+ appStringId = LanguageSupportDefinition.LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "he"
+ }.build()
+ }.build()
+ }.build()
+ regionDefinition = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.UNITED_STATES
+ regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "US"
+ }.build()
+ }.build()
+ }.build()
+
+ private fun String.extractNumbers(): List =
+ "\\d+".toRegex().findAll(this).flatMap { it.groupValues }.toList()
+ }
+}
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/LanguageConfigRetrieverTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/LanguageConfigRetrieverTest.kt
new file mode 100644
index 00000000000..2de90f0971d
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/locale/LanguageConfigRetrieverTest.kt
@@ -0,0 +1,267 @@
+package org.oppia.android.domain.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.oppia.android.app.model.SupportedLanguages
+import org.oppia.android.app.model.SupportedRegions
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.util.caching.AssetModule
+import org.oppia.android.util.logging.LoggerModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [LanguageConfigRetriever]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class LanguageConfigRetrieverTest {
+ @Inject
+ lateinit var languageConfigRetriever: LanguageConfigRetriever
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testOppiaLanguage_hasSupportForSixLanguages() {
+ // While it's a bit strange to test a proto, and particularly in this file, this suite is
+ // generally responsible for verifying language & region configuration sanity. Part of that
+ // requires verifying that all languages are tested below. Note that '8' is because the base
+ // 6 languages are supported + LANGUAGE_UNSPECIFIED and UNRECOGNIZED (auto-generated by
+ // Protobuf). Finally, note that the values themselves are not checked since it doesn't provide
+ // any benefit (being able to reference an enum constant without a compiler error is sufficient
+ // proof that constant is available).
+ assertThat(OppiaLanguage.values()).hasLength(8)
+ }
+
+ @Test
+ fun testOppiaRegion_hasSupportForSixLanguages() {
+ // See above test for context on why this test is here & for why the number is correct.
+ assertThat(OppiaRegion.values()).hasLength(5)
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_loadsNonDefaultProtoFromAssets() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ assertThat(supportedLanguages).isNotEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_hasSixSupportedLanguages() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ // Change detector test to ensure changes to the configuration are reflected in tests since
+ // changes to the configuration can have a major impact on the app (and may require additional
+ // work to be done to support the changes).
+ assertThat(supportedLanguages.languageDefinitionsCount).isEqualTo(6)
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_arabic_isSupportedForAppContentAudioTranslations() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ val definition = supportedLanguages.lookUpLanguage(OppiaLanguage.ARABIC)
+ assertThat(definition.hasAppStringId()).isTrue()
+ assertThat(definition.hasContentStringId()).isTrue()
+ assertThat(definition.hasAudioTranslationId()).isTrue()
+ assertThat(definition.fallbackMacroLanguage).isEqualTo(OppiaLanguage.LANGUAGE_UNSPECIFIED)
+ assertThat(definition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("ar")
+ assertThat(definition.appStringId.androidResourcesLanguageId.languageCode).isEqualTo("ar")
+ assertThat(definition.appStringId.androidResourcesLanguageId.regionCode).isEmpty()
+ assertThat(definition.contentStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("ar")
+ assertThat(definition.audioTranslationId.ietfBcp47Id.ietfLanguageTag).isEqualTo("ar")
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_english_isSupportedForAppContentAudioTranslations() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ val definition = supportedLanguages.lookUpLanguage(OppiaLanguage.ENGLISH)
+ assertThat(definition.hasAppStringId()).isTrue()
+ assertThat(definition.hasContentStringId()).isTrue()
+ assertThat(definition.hasAudioTranslationId()).isTrue()
+ assertThat(definition.fallbackMacroLanguage).isEqualTo(OppiaLanguage.LANGUAGE_UNSPECIFIED)
+ assertThat(definition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ assertThat(definition.appStringId.androidResourcesLanguageId.languageCode).isEqualTo("en")
+ assertThat(definition.appStringId.androidResourcesLanguageId.regionCode).isEmpty()
+ assertThat(definition.contentStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ assertThat(definition.audioTranslationId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_hindi_isSupportedForAppContentAudioTranslations() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ val definition = supportedLanguages.lookUpLanguage(OppiaLanguage.HINDI)
+ assertThat(definition.hasAppStringId()).isTrue()
+ assertThat(definition.hasContentStringId()).isTrue()
+ assertThat(definition.hasAudioTranslationId()).isTrue()
+ assertThat(definition.fallbackMacroLanguage).isEqualTo(OppiaLanguage.LANGUAGE_UNSPECIFIED)
+ assertThat(definition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("hi")
+ assertThat(definition.appStringId.androidResourcesLanguageId.languageCode).isEqualTo("hi")
+ assertThat(definition.appStringId.androidResourcesLanguageId.regionCode).isEmpty()
+ assertThat(definition.contentStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("hi")
+ assertThat(definition.audioTranslationId.ietfBcp47Id.ietfLanguageTag).isEqualTo("hi")
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_hinglish_isSupportedForContentAndAudioTranslationsWithFallback() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ val definition = supportedLanguages.lookUpLanguage(OppiaLanguage.HINGLISH)
+ assertThat(definition.hasAppStringId()).isFalse()
+ assertThat(definition.hasContentStringId()).isTrue()
+ assertThat(definition.hasAudioTranslationId()).isTrue()
+ assertThat(definition.fallbackMacroLanguage).isEqualTo(OppiaLanguage.ENGLISH)
+ assertThat(definition.contentStringId.macaronicId.combinedLanguageCode).isEqualTo("hi-en")
+ assertThat(definition.audioTranslationId.macaronicId.combinedLanguageCode).isEqualTo("hi-en")
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_portuguese_hasNoTranslationSupport() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ val definition = supportedLanguages.lookUpLanguage(OppiaLanguage.PORTUGUESE)
+ assertThat(definition.hasAppStringId()).isFalse()
+ assertThat(definition.hasContentStringId()).isFalse()
+ assertThat(definition.hasAudioTranslationId()).isFalse()
+ assertThat(definition.fallbackMacroLanguage).isEqualTo(OppiaLanguage.LANGUAGE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testLoadSupportedLangs_brazilianPortuguese_supportsAppContentAudioTranslationsWithFallback() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ val definition = supportedLanguages.lookUpLanguage(OppiaLanguage.BRAZILIAN_PORTUGUESE)
+ assertThat(definition.hasAppStringId()).isTrue()
+ assertThat(definition.hasContentStringId()).isTrue()
+ assertThat(definition.hasAudioTranslationId()).isTrue()
+ assertThat(definition.fallbackMacroLanguage).isEqualTo(OppiaLanguage.PORTUGUESE)
+ assertThat(definition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("pt-BR")
+ assertThat(definition.appStringId.androidResourcesLanguageId.languageCode).isEqualTo("pt")
+ assertThat(definition.appStringId.androidResourcesLanguageId.regionCode).isEqualTo("BR")
+ assertThat(definition.contentStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("pt-BR")
+ assertThat(definition.audioTranslationId.ietfBcp47Id.ietfLanguageTag).isEqualTo("pt-BR")
+ }
+
+ @Test
+ fun testLoadSupportedRegions_loadsNonDefaultProtoFromAssets() {
+ val supportedRegions = languageConfigRetriever.loadSupportedRegions()
+
+ assertThat(supportedRegions).isNotEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testLoadSupportedRegions_hasThreeSupportedRegions() {
+ val supportedRegions = languageConfigRetriever.loadSupportedRegions()
+
+ // Change detector test to ensure changes to the configuration are reflected in tests since
+ // changes to the configuration can have a major impact on the app (and may require additional
+ // work to be done to support the changes).
+ assertThat(supportedRegions.regionDefinitionsCount).isEqualTo(3)
+ }
+
+ @Test
+ fun testLoadSupportedRegions_brazil_hasCorrectRegionIdAndSupportedLanguages() {
+ val supportedRegions = languageConfigRetriever.loadSupportedRegions()
+
+ val definition = supportedRegions.lookUpRegion(OppiaRegion.BRAZIL)
+ assertThat(definition.regionId.ietfRegionTag).isEqualTo("BR")
+ assertThat(definition.languagesList)
+ .containsExactly(OppiaLanguage.PORTUGUESE, OppiaLanguage.BRAZILIAN_PORTUGUESE)
+ }
+
+ @Test
+ fun testLoadSupportedRegions_india_hasCorrectRegionIdAndSupportedLanguages() {
+ val supportedRegions = languageConfigRetriever.loadSupportedRegions()
+
+ val definition = supportedRegions.lookUpRegion(OppiaRegion.INDIA)
+ assertThat(definition.regionId.ietfRegionTag).isEqualTo("IN")
+ assertThat(definition.languagesList)
+ .containsExactly(OppiaLanguage.HINDI, OppiaLanguage.HINGLISH)
+ }
+
+ @Test
+ fun testLoadSupportedRegions_unitedStates_hasCorrectRegionIdAndSupportedLanguages() {
+ val supportedRegions = languageConfigRetriever.loadSupportedRegions()
+
+ val definition = supportedRegions.lookUpRegion(OppiaRegion.UNITED_STATES)
+ assertThat(definition.regionId.ietfRegionTag).isEqualTo("US")
+ assertThat(definition.languagesList).containsExactly(OppiaLanguage.ENGLISH)
+ }
+
+ private fun SupportedLanguages.lookUpLanguage(
+ language: OppiaLanguage
+ ): LanguageSupportDefinition {
+ val definition = languageDefinitionsList.find { it.language == language }
+ // Sanity check.
+ assertThat(definition).isNotNull()
+ return definition!!
+ }
+
+ private fun SupportedRegions.lookUpRegion(region: OppiaRegion): RegionSupportDefinition {
+ val definition = regionDefinitionsList.find { it.region == region }
+ // Sanity check.
+ assertThat(definition).isNotNull()
+ return definition!!
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerLanguageConfigRetrieverTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LoggerModule::class, TestDispatcherModule::class, RobolectricModule::class,
+ AssetModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(languageConfigRetrieverTest: LanguageConfigRetrieverTest)
+ }
+}
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/LanguageConfigRetrieverWithoutAssetsTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/LanguageConfigRetrieverWithoutAssetsTest.kt
new file mode 100644
index 00000000000..ff5b65014d5
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/locale/LanguageConfigRetrieverWithoutAssetsTest.kt
@@ -0,0 +1,96 @@
+package org.oppia.android.domain.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.util.caching.testing.AssetTestNoOpModule
+import org.oppia.android.util.logging.LoggerModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+// TODO(#59): Use a build-time configuration instead of the runtime AssetRepository fake (i.e. by
+// not including the config assets during build time).
+/**
+ * Tests for [LanguageConfigRetriever]. Unlike [LanguageConfigRetrieverTest], this suite verifies
+ * the retriever's behavior when there are no configuration files to include.
+ */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class LanguageConfigRetrieverWithoutAssetsTest {
+ @Inject
+ lateinit var languageConfigRetriever: LanguageConfigRetriever
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testLoadSupportedLanguages_withoutAssets_returnsDefaultInstance() {
+ val supportedLanguages = languageConfigRetriever.loadSupportedLanguages()
+
+ // Using the no-op asset repository results in no assets being present.
+ assertThat(supportedLanguages).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testLoadSupportedRegions_withoutAssets_returnsDefaultInstance() {
+ val supportedRegions = languageConfigRetriever.loadSupportedRegions()
+
+ // Using the no-op asset repository results in no assets being present.
+ assertThat(supportedRegions).isEqualToDefaultInstance()
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerLanguageConfigRetrieverWithoutAssetsTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LoggerModule::class, TestDispatcherModule::class, RobolectricModule::class,
+ AssetTestNoOpModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(languageConfigRetrieverWithoutAssetsTest: LanguageConfigRetrieverWithoutAssetsTest)
+ }
+}
diff --git a/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt
new file mode 100644
index 00000000000..3f61ad22bfa
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt
@@ -0,0 +1,893 @@
+package org.oppia.android.domain.locale
+
+import android.app.Application
+import android.content.Context
+import android.content.res.Configuration
+import android.os.LocaleList
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE
+import org.oppia.android.app.model.OppiaLanguage.ENGLISH
+import org.oppia.android.app.model.OppiaLanguage.HINDI
+import org.oppia.android.app.model.OppiaLanguage.HINGLISH
+import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+import org.oppia.android.app.model.OppiaRegion.INDIA
+import org.oppia.android.app.model.OppiaRegion.REGION_UNSPECIFIED
+import org.oppia.android.app.model.OppiaRegion.UNITED_STATES
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.assertThrows
+import org.oppia.android.testing.data.DataProviderTestMonitor
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
+import org.oppia.android.util.data.DataProvidersInjector
+import org.oppia.android.util.data.DataProvidersInjectorProvider
+import org.oppia.android.util.locale.LocaleProdModule
+import org.oppia.android.util.locale.OppiaLocale
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import org.robolectric.shadows.ShadowLog
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Tests for [LocaleController].
+ *
+ * Note that these tests depend on real locales being present in the local environment
+ * (Robolectric).
+ */
+// FunctionName: test names are conventionally named with underscores.
+// SameParameterValue: tests should have specific context included/excluded for readability.
+@Suppress("FunctionName", "SameParameterValue")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = LocaleControllerTest.TestApplication::class)
+class LocaleControllerTest {
+ @Rule
+ @JvmField
+ val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Inject
+ lateinit var localeController: LocaleController
+
+ @Inject
+ lateinit var monitorFactory: DataProviderTestMonitor.Factory
+
+ @Inject
+ lateinit var context: Context
+
+ @Mock
+ lateinit var mockDisplayLocale: OppiaLocale.DisplayLocale
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ /* Tests for getLikelyDefaultAppStringLocaleContext & reconstituteDisplayLocale. */
+
+ @Test
+ fun testGetLikelyDefaultAppStringLocaleContext_returnsAppStringContextForEnglish() {
+ val context = localeController.getLikelyDefaultAppStringLocaleContext()
+
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.languageDefinition.minAndroidSdkVersion).isEqualTo(1)
+ assertThat(context.languageDefinition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(UNITED_STATES)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("US")
+ assertThat(context.usageMode).isEqualTo(OppiaLocaleContext.LanguageUsageMode.APP_STRINGS)
+ }
+
+ @Test
+ fun testGetLikelyDefaultAppStringLocaleContext_twice_returnsSameContext() {
+ val firstContext = localeController.getLikelyDefaultAppStringLocaleContext()
+ val secondContext = localeController.getLikelyDefaultAppStringLocaleContext()
+
+ assertThat(firstContext).isEqualTo(secondContext)
+ }
+
+ @Test
+ fun testReconstituteDisplayLocale_defaultContext_returnsDisplayLocaleForContext() {
+ val context = OppiaLocaleContext.getDefaultInstance()
+
+ val locale = localeController.reconstituteDisplayLocale(context)
+
+ assertThat(locale.localeContext).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testReconstituteDisplayLocale_nonDefaultContext_returnsDisplayLocaleForContext() {
+ val context = localeController.getLikelyDefaultAppStringLocaleContext()
+
+ val locale = localeController.reconstituteDisplayLocale(context)
+
+ assertThat(locale.localeContext).isEqualTo(context)
+ }
+
+ /* Tests for retrieveAppStringDisplayLocale. */
+
+ @Test
+ fun testAppStringLocale_rootLocale_noConfigLocale_printsErrorForDefaulting() {
+ context.applicationContext.resources.configuration.setLocale(null)
+ Locale.setDefault(Locale.ROOT)
+
+ monitorFactory.waitForNextSuccessfulResult(
+ localeController.retrieveAppStringDisplayLocale(ENGLISH)
+ )
+
+ assertThat(retrieveLogcatLogs())
+ .contains("No locales defined for application context. Defaulting to default Locale.")
+ }
+
+ @Test
+ fun testAppStringLocale_rootLocale_english_hasCorrectLanguageAndFallbackDefinitions() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.languageDefinition.fallbackMacroLanguage).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(context.languageDefinition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testAppStringLocale_usLocale_english_hasMatchedUsRegion() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ assertThat(locale.localeContext.regionDefinition.region).isEqualTo(UNITED_STATES)
+ }
+
+ @Test
+ fun testAppStringLocale_monacoLocale_english_hasUnmatchedMonacoRegion() {
+ forceDefaultLocale(MONACO_FRENCH_LOCALE)
+
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ assertThat(locale.localeContext.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testAppStringLocale_indiaLocale_english_printsRegionLanguageMismatchError() {
+ forceDefaultLocale(INDIA_HINDI_LOCALE)
+
+ monitorFactory.waitForNextSuccessfulResult(
+ localeController.retrieveAppStringDisplayLocale(ENGLISH)
+ )
+
+ assertThat(retrieveLogcatLogs())
+ .contains(
+ "Notice: selected language $ENGLISH is not part of the corresponding region matched to" +
+ " this locale: $INDIA (ID: IN) (supported languages: [$HINDI, $HINGLISH]"
+ )
+ }
+
+ @Test
+ fun testAppStringLocale_englishUsLocale_defaultLang_returnsEnglishLocale() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(LANGUAGE_UNSPECIFIED)
+
+ // English should be matched per the system locale.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.languageDefinition.fallbackMacroLanguage).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(context.languageDefinition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(UNITED_STATES)
+ }
+
+ @Test
+ fun testAppStringLocale_englishUsLocale_defaultLang_printsError() {
+ forceDefaultLocale(Locale.US)
+
+ monitorFactory.waitForNextSuccessfulResult(
+ localeController.retrieveAppStringDisplayLocale(LANGUAGE_UNSPECIFIED)
+ )
+
+ assertThat(retrieveLogcatLogs())
+ .contains("Encountered unmatched language: $LANGUAGE_UNSPECIFIED")
+ }
+
+ @Test
+ fun testAppStringLocale_frenchMonacoLocale_defaultLang_returnsFrenchDefaultLocale() {
+ forceDefaultLocale(MONACO_FRENCH_LOCALE)
+
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(LANGUAGE_UNSPECIFIED)
+
+ // The locale will be forced to fit Monaco & French despite not being directly supported by the
+ // app.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(context.languageDefinition.fallbackMacroLanguage).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(context.languageDefinition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("fr")
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("MC")
+ }
+
+ @Test
+ fun testAppStringLocale_newSystemLocale_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.CANADA)
+
+ // Simply changing the locale shouldn't change the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testAppStringLocale_notifyPotentialLocaleChange_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ localeController.notifyPotentialLocaleChange()
+
+ // Just notifying isn't sufficient to trigger a change in the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testAppStringLocale_newSystemLocale_sameRegion_notify_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.US)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing & notifying for the same locale should not change the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testAppStringLocale_newSystemLocale_newRegion_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.CANADA)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing to a new region (but keeping the same language) should update the region.
+ val locale = monitor.waitForNextSuccessResult()
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("CA")
+ }
+
+ @Test
+ fun testAppStringLocale_newSystemLocale_defLangMatching_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.ROOT)
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(LANGUAGE_UNSPECIFIED)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(INDIA_HINDI_LOCALE)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing to a matched language should update the provider result.
+ val locale = monitor.waitForNextSuccessResult()
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(HINDI)
+ assertThat(context.regionDefinition.region).isEqualTo(INDIA)
+ }
+
+ @Test
+ fun testAppStringLocale_newSystemLocale_defLangUnmatching_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.ROOT)
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(LANGUAGE_UNSPECIFIED)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(MONACO_FRENCH_LOCALE)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing to an unmatched language should update the provider.
+ val locale = monitor.waitForNextSuccessResult()
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(context.languageDefinition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("fr")
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("MC")
+ }
+
+ /* Tests for retrieveWrittenTranslationsLocale. */
+
+ @Test
+ fun testContentLocale_rootLocale_noConfigLocale_printsErrorForDefaulting() {
+ context.applicationContext.resources.configuration.setLocale(null)
+ Locale.setDefault(Locale.ROOT)
+
+ monitorFactory.waitForNextSuccessfulResult(
+ localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+ )
+
+ assertThat(retrieveLogcatLogs())
+ .contains("No locales defined for application context. Defaulting to default Locale.")
+ }
+
+ @Test
+ fun testContentLocale_rootLocale_english_hasCorrectLanguageAndFallbackDefinitions() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.languageDefinition.fallbackMacroLanguage).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(context.languageDefinition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testContentLocale_usLocale_english_hasMatchedUsRegion() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ assertThat(locale.localeContext.regionDefinition.region).isEqualTo(UNITED_STATES)
+ }
+
+ @Test
+ fun testContentLocale_monacoLocale_english_hasUnmatchedMonacoRegion() {
+ forceDefaultLocale(MONACO_FRENCH_LOCALE)
+
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ assertThat(locale.localeContext.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testContentLocale_indiaLocale_english_printsRegionLanguageMismatchError() {
+ forceDefaultLocale(INDIA_HINDI_LOCALE)
+
+ monitorFactory.waitForNextSuccessfulResult(
+ localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+ )
+
+ assertThat(retrieveLogcatLogs())
+ .contains(
+ "Notice: selected language $ENGLISH is not part of the corresponding region matched to" +
+ " this locale: $INDIA (ID: IN) (supported languages: [$HINDI, $HINGLISH]"
+ )
+ }
+
+ @Test
+ fun testContentLocale_englishUsLocale_defaultLang_returnsFailure() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(LANGUAGE_UNSPECIFIED)
+
+ // English should be matched per the system locale.
+ val error = monitorFactory.waitForNextFailureResult(localeProvider)
+ assertThat(error)
+ .hasMessageThat()
+ .contains(
+ "Language $LANGUAGE_UNSPECIFIED for usage $CONTENT_STRINGS doesn't match supported" +
+ " language definitions"
+ )
+ }
+
+ @Test
+ fun testContentLocale_englishUsLocale_defaultLang_printsError() {
+ forceDefaultLocale(Locale.US)
+
+ val monitor = monitorFactory.createMonitor(
+ localeController.retrieveWrittenTranslationsLocale(LANGUAGE_UNSPECIFIED)
+ )
+ monitor.waitForNextResult()
+
+ assertThat(retrieveLogcatLogs())
+ .contains("Encountered unmatched language: $LANGUAGE_UNSPECIFIED")
+ }
+
+ @Test
+ fun testContentLocale_newSystemLocale_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.CANADA)
+
+ // Simply changing the locale shouldn't change the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testContentLocale_notifyPotentialLocaleChange_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ localeController.notifyPotentialLocaleChange()
+
+ // Just notifying isn't sufficient to trigger a change in the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testContentLocale_newSystemLocale_sameRegion_notify_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.US)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing & notifying for the same locale should not change the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testContentLocale_newSystemLocale_newRegion_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveWrittenTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.CANADA)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing to a new region (but keeping the same language) should update the region.
+ val locale = monitor.waitForNextSuccessResult()
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("CA")
+ }
+
+ /* Tests for retrieveAudioTranslationsLocale. */
+
+ @Test
+ fun testAudioLocale_rootLocale_noConfigLocale_printsErrorForDefaulting() {
+ context.applicationContext.resources.configuration.setLocale(null)
+ Locale.setDefault(Locale.ROOT)
+
+ monitorFactory.waitForNextSuccessfulResult(
+ localeController.retrieveAudioTranslationsLocale(ENGLISH)
+ )
+
+ assertThat(retrieveLogcatLogs())
+ .contains("No locales defined for application context. Defaulting to default Locale.")
+ }
+
+ @Test
+ fun testAudioLocale_rootLocale_english_hasCorrectLanguageAndFallbackDefinitions() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.languageDefinition.fallbackMacroLanguage).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(context.languageDefinition.appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("en")
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testAudioLocale_usLocale_english_hasMatchedUsRegion() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ assertThat(locale.localeContext.regionDefinition.region).isEqualTo(UNITED_STATES)
+ }
+
+ @Test
+ fun testAudioLocale_monacoLocale_english_hasUnmatchedMonacoRegion() {
+ forceDefaultLocale(MONACO_FRENCH_LOCALE)
+
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(ENGLISH)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ assertThat(locale.localeContext.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testAudioLocale_indiaLocale_english_printsRegionLanguageMismatchError() {
+ forceDefaultLocale(INDIA_HINDI_LOCALE)
+
+ monitorFactory.waitForNextSuccessfulResult(
+ localeController.retrieveAudioTranslationsLocale(ENGLISH)
+ )
+
+ assertThat(retrieveLogcatLogs())
+ .contains(
+ "Notice: selected language $ENGLISH is not part of the corresponding region matched to" +
+ " this locale: $INDIA (ID: IN) (supported languages: [$HINDI, $HINGLISH]"
+ )
+ }
+
+ @Test
+ fun testAudioLocale_englishUsLocale_defaultLang_returnsFailure() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(LANGUAGE_UNSPECIFIED)
+
+ // English should be matched per the system locale.
+ val error = monitorFactory.waitForNextFailureResult(localeProvider)
+ assertThat(error)
+ .hasMessageThat()
+ .contains(
+ "Language $LANGUAGE_UNSPECIFIED for usage $AUDIO_TRANSLATIONS doesn't match supported" +
+ " language definitions"
+ )
+ }
+
+ @Test
+ fun testAudioLocale_englishUsLocale_defaultLang_printsError() {
+ forceDefaultLocale(Locale.US)
+
+ val monitor = monitorFactory.createMonitor(
+ localeController.retrieveAudioTranslationsLocale(LANGUAGE_UNSPECIFIED)
+ )
+ monitor.waitForNextResult()
+
+ assertThat(retrieveLogcatLogs())
+ .contains("Encountered unmatched language: $LANGUAGE_UNSPECIFIED")
+ }
+
+ @Test
+ fun testAudioLocale_newSystemLocale_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.CANADA)
+
+ // Simply changing the locale shouldn't change the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testAudioLocale_notifyPotentialLocaleChange_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ localeController.notifyPotentialLocaleChange()
+
+ // Just notifying isn't sufficient to trigger a change in the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testAudioLocale_newSystemLocale_sameRegion_notify_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.US)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing & notifying for the same locale should not change the provider.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testAudioLocale_newSystemLocale_newRegion_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = localeController.retrieveAudioTranslationsLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ forceDefaultLocale(Locale.CANADA)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing to a new region (but keeping the same language) should update the region.
+ val locale = monitor.waitForNextSuccessResult()
+ val context = locale.localeContext
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("CA")
+ }
+
+ /* Tests for getSystemLocaleProfile. */
+
+ @Test
+ fun testSystemLanguage_rootLocale_returnsUnspecifiedLanguage() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val languageProvider = localeController.retrieveSystemLanguage()
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testSystemLanguage_usEnglishLocale_returnsEnglish() {
+ forceDefaultLocale(Locale.US)
+
+ val languageProvider = localeController.retrieveSystemLanguage()
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testSystemLanguage_indiaEnglishLocale_returnsEnglish() {
+ forceDefaultLocale(INDIA_ENGLISH_LOCALE)
+
+ val languageProvider = localeController.retrieveSystemLanguage()
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testSystemLanguage_frenchLocale_returnsUnspecifiedLanguage() {
+ forceDefaultLocale(Locale.FRENCH)
+
+ val languageProvider = localeController.retrieveSystemLanguage()
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testSystemLanguage_notifyPotentialLocaleChange_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.FRENCH)
+ val languageProvider = localeController.retrieveSystemLanguage()
+ val monitor = monitorFactory.createMonitor(languageProvider)
+ monitor.waitForNextResult()
+
+ localeController.notifyPotentialLocaleChange()
+
+ // The provider shouldn't be updated just for a notification with no state change.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testSystemLanguage_changeLocale_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.FRENCH)
+ val languageProvider = localeController.retrieveSystemLanguage()
+ val monitor = monitorFactory.createMonitor(languageProvider)
+ monitor.waitForNextResult()
+
+ forceDefaultLocale(Locale.ENGLISH)
+
+ // Changing the locale isn't sufficient to update the provider without a notification (since
+ // Android doesn't provide a way to monitor for locale without utilizing the configuration
+ // change system).
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ @Test
+ fun testSystemLanguage_englishToPortuguese_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.US)
+ val languageProvider = localeController.retrieveSystemLanguage()
+ val monitor = monitorFactory.createMonitor(languageProvider)
+ monitor.waitForNextResult()
+
+ forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
+ localeController.notifyPotentialLocaleChange()
+
+ // The notify + locale change should change the reported system language.
+ val language = monitor.waitForNextSuccessResult()
+ assertThat(language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ }
+
+ @Test
+ fun testSystemLanguage_englishToFrench_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.US)
+ val languageProvider = localeController.retrieveSystemLanguage()
+ val monitor = monitorFactory.createMonitor(languageProvider)
+ monitor.waitForNextResult()
+
+ forceDefaultLocale(Locale.FRENCH)
+ localeController.notifyPotentialLocaleChange()
+
+ // A known language (English) can change to an unspecified language.
+ val language = monitor.waitForNextSuccessResult()
+ assertThat(language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testSystemLanguage_frenchToEnglish_notify_notifiesProvider() {
+ forceDefaultLocale(Locale.FRENCH)
+ val languageProvider = localeController.retrieveSystemLanguage()
+ val monitor = monitorFactory.createMonitor(languageProvider)
+ monitor.waitForNextResult()
+
+ forceDefaultLocale(Locale.US)
+ localeController.notifyPotentialLocaleChange()
+
+ // An unspecified language can change to a known language (English).
+ val language = monitor.waitForNextSuccessResult()
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testSystemLanguage_frenchToGerman_notify_doesNotNotifyProvider() {
+ forceDefaultLocale(Locale.FRENCH)
+ val languageProvider = localeController.retrieveSystemLanguage()
+ val monitor = monitorFactory.createMonitor(languageProvider)
+ monitor.waitForNextResult()
+
+ forceDefaultLocale(Locale.GERMAN)
+ localeController.notifyPotentialLocaleChange()
+
+ // Changing from one unspecified language to another shouldn't notify the provider since the
+ // outcome is the same.
+ monitor.verifyProviderIsNotUpdated()
+ }
+
+ /* Tests for setAsDefault. */
+
+ @Test
+ fun testSetAsDefault_customLocaleImpl_throwsException() {
+ val exception = assertThrows(IllegalStateException::class) {
+ localeController.setAsDefault(mockDisplayLocale, Configuration())
+ }
+
+ assertThat(exception).hasMessageThat().contains("Invalid display locale type passed in")
+ }
+
+ @Test
+ fun testSetAsDefault_englishDisplayLocale_changesLocaleInConfiguration() {
+ forceDefaultLocale(Locale.ROOT)
+ val configuration = Configuration()
+ val locale = retrieveAppStringDisplayLocale(ENGLISH)
+ // Sanity check (to validate the configuration was changed).
+ assertThat(configuration.locales.hasLanguage("en")).isFalse()
+
+ localeController.setAsDefault(locale, configuration)
+
+ // Verify that the configuration's locale has changed.
+ assertThat(configuration.locales.hasLanguage("en")).isTrue()
+ }
+
+ @Test
+ fun testSetAsDefault_englishDisplayLocale_changesSystemDefaultLocale() {
+ forceDefaultLocale(Locale.ROOT)
+ val locale = retrieveAppStringDisplayLocale(ENGLISH)
+ // Sanity check (to validate the system locale was changed).
+ assertThat(Locale.getDefault().language).isNotEqualTo("en")
+
+ localeController.setAsDefault(locale, Configuration())
+
+ // Verify that the default locale has changed.
+ assertThat(Locale.getDefault().language).isEqualTo("en")
+ }
+
+ @Test
+ fun testSetAsDefault_englishDisplayLocale_triggersChangeInSystemLanguageProvider() {
+ forceDefaultLocale(Locale.ROOT)
+ val locale = retrieveAppStringDisplayLocale(ENGLISH)
+ val monitor = monitorFactory.createMonitor(localeController.retrieveSystemLanguage())
+ // Sanity check (to validate the system language actually changes).
+ assertThat(monitor.waitForNextSuccessResult()).isNotEqualTo(ENGLISH)
+
+ localeController.setAsDefault(locale, Configuration())
+
+ // Verify that the system language provider is notified with the language change.
+ assertThat(monitor.waitForNextSuccessResult()).isEqualTo(ENGLISH)
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ private fun forceDefaultLocale(locale: Locale) {
+ context.applicationContext.resources.configuration.setLocale(locale)
+ Locale.setDefault(locale)
+ }
+
+ private fun retrieveAppStringDisplayLocale(language: OppiaLanguage): OppiaLocale.DisplayLocale {
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(language)
+ return monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ }
+
+ private fun retrieveLogcatLogs(): List = ShadowLog.getLogs().map { it.msg }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LogStorageModule::class, NetworkConnectionUtilDebugModule::class,
+ TestLogReportingModule::class, LoggerModule::class, TestDispatcherModule::class,
+ LocaleProdModule::class, FakeOppiaClockModule::class, RobolectricModule::class,
+ AssetModule::class
+ ]
+ )
+ interface TestApplicationComponent : DataProvidersInjector {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(localeControllerTest: LocaleControllerTest)
+ }
+
+ class TestApplication : Application(), DataProvidersInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerLocaleControllerTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build()
+ }
+
+ fun inject(localeControllerTest: LocaleControllerTest) {
+ component.inject(localeControllerTest)
+ }
+
+ override fun getDataProvidersInjector(): DataProvidersInjector = component
+ }
+
+ private companion object {
+ private val MONACO_FRENCH_LOCALE = Locale("fr", "MC")
+ private val INDIA_HINDI_LOCALE = Locale("hi", "IN")
+ private val INDIA_ENGLISH_LOCALE = Locale("en", "IN")
+ private val BRAZIL_PORTUGUESE_LOCALE = Locale("pt", "BR")
+
+ private fun LocaleList.toList(): List = (0 until size()).map { this[it] }
+ private fun LocaleList.hasLanguage(languageCode: String): Boolean =
+ toList().any { it.language == languageCode }
+ }
+}
diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt
index 015d60584d4..096a377c889 100644
--- a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt
@@ -58,6 +58,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
@@ -1882,7 +1883,7 @@ class QuestionAssessmentProgressControllerTest {
ImageClickInputModule::class, LogStorageModule::class, TestDispatcherModule::class,
RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
CachingTestModule::class, HintsAndSolutionConfigModule::class,
- HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class
+ HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt
index e243d66b9e7..446d71be82c 100644
--- a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt
@@ -47,6 +47,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
@@ -336,7 +337,7 @@ class QuestionTrainingControllerTest {
LogStorageModule::class, TestDispatcherModule::class, RatioInputModule::class,
RobolectricModule::class, FakeOppiaClockModule::class, CachingTestModule::class,
HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class,
- NetworkConnectionUtilDebugModule::class
+ NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt
index a64946d8ff2..e81bbbcf07b 100755
--- a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt
@@ -42,6 +42,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.caching.TopicListToCache
@@ -56,7 +57,6 @@ import org.oppia.android.util.logging.LogLevel
import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
-import java.io.FileNotFoundException
import java.lang.IllegalStateException
import javax.inject.Inject
import javax.inject.Singleton
@@ -1143,7 +1143,8 @@ class TopicControllerTest {
val exception = fakeExceptionLogger.getMostRecentException()
- assertThat(exception).isInstanceOf(FileNotFoundException::class.java)
+ assertThat(exception).isInstanceOf(IllegalStateException::class.java)
+ assertThat(exception).hasMessageThat().contains("Asset doesn't exist")
}
private fun setUpTestApplicationComponent() {
@@ -1280,7 +1281,7 @@ class TopicControllerTest {
modules = [
TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
TestDispatcherModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
- NetworkConnectionUtilDebugModule::class
+ NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt
index 95e5ea469e9..457c4c7f36c 100644
--- a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt
@@ -36,6 +36,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.caching.TopicListToCache
@@ -872,7 +873,7 @@ class TopicListControllerTest {
modules = [
TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
TestDispatcherModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
- NetworkConnectionUtilDebugModule::class
+ NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel
new file mode 100644
index 00000000000..d72e295975e
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel
@@ -0,0 +1,36 @@
+"""
+Tests for domain components pertaining to managing translations.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "TranslationControllerTest",
+ srcs = ["TranslationControllerTest.kt"],
+ custom_package = "org.oppia.android.domain.translation",
+ test_class = "org.oppia.android.domain.translation.TranslationControllerTest",
+ test_manifest = "//domain:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller",
+ "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller",
+ "//model:languages_java_proto_lite",
+ "//testing",
+ "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_mockito_mockito-core",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ "//utility/src/main/java/org/oppia/android/util/networking:debug_module",
+ ],
+)
+
+dagger_rules()
diff --git a/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt
new file mode 100644
index 00000000000..936f34efd73
--- /dev/null
+++ b/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt
@@ -0,0 +1,1146 @@
+package org.oppia.android.domain.translation
+
+import android.app.Application
+import android.content.Context
+import android.content.res.Configuration
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.AppLanguageSelection
+import org.oppia.android.app.model.AudioTranslationLanguageSelection
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE
+import org.oppia.android.app.model.OppiaLanguage.ENGLISH
+import org.oppia.android.app.model.OppiaLanguage.HINDI
+import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+import org.oppia.android.app.model.OppiaRegion.BRAZIL
+import org.oppia.android.app.model.OppiaRegion.REGION_UNSPECIFIED
+import org.oppia.android.app.model.OppiaRegion.UNITED_STATES
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.SubtitledHtml
+import org.oppia.android.app.model.SubtitledUnicode
+import org.oppia.android.app.model.Translation
+import org.oppia.android.app.model.WrittenTranslationContext
+import org.oppia.android.app.model.WrittenTranslationLanguageSelection
+import org.oppia.android.domain.locale.LocaleController
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.data.DataProviderTestMonitor
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
+import org.oppia.android.util.data.DataProvidersInjector
+import org.oppia.android.util.data.DataProvidersInjectorProvider
+import org.oppia.android.util.locale.LocaleProdModule
+import org.oppia.android.util.locale.OppiaLocale
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [TranslationController]. */
+// FunctionName: test names are conventionally named with underscores.
+// SameParameterValue: tests should have specific context included/excluded for readability.
+@Suppress("FunctionName", "SameParameterValue")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = TranslationControllerTest.TestApplication::class)
+class TranslationControllerTest {
+ @Inject
+ lateinit var translationController: TranslationController
+
+ @Inject
+ lateinit var localeController: LocaleController
+
+ @Inject
+ lateinit var monitorFactory: DataProviderTestMonitor.Factory
+
+ @Inject
+ lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ /* Tests for getSystemLanguageLocale */
+
+ @Test
+ fun testGetSystemLanguageLocale_rootLocale_returnsLocaleWithBlankContext() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = translationController.getSystemLanguageLocale()
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ val appStringId = context.languageDefinition.appStringId
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(appStringId.languageTypeCase).isEqualTo(IETF_BCP47_ID)
+ assertThat(appStringId.ietfBcp47Id.ietfLanguageTag).isEmpty()
+ assertThat(appStringId.androidResourcesLanguageId.languageCode).isEmpty()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testGetSystemLanguageLocale_usLocale_returnsLocaleWithEnglishContext() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = translationController.getSystemLanguageLocale()
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.regionDefinition.region).isEqualTo(UNITED_STATES)
+ }
+
+ @Test
+ fun testGetSystemLanguageLocale_updateLocaleToIndia_updatesProviderWithNewLocale() {
+ forceDefaultLocale(Locale.US)
+ val localeProvider = translationController.getSystemLanguageLocale()
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ localeController.setAsDefault(createDisplayLocaleForLanguage(HINDI), Configuration())
+
+ // Verify that the provider has changed.
+ val locale = monitor.waitForNextSuccessResult()
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(HINDI)
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ /* Tests for app language functions */
+
+ @Test
+ fun testUpdateAppLanguage_returnsSuccess() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val resultProvider =
+ translationController.updateAppLanguage(PROFILE_ID_0, createAppLanguageSelection(ENGLISH))
+
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ @Test
+ fun testUpdateAppLanguage_notifiesProviderWithChange() {
+ forceDefaultLocale(Locale.ROOT)
+ val languageProvider = translationController.getAppLanguage(PROFILE_ID_0)
+ val langMonitor = monitorFactory.createMonitor(languageProvider)
+ langMonitor.waitForNextSuccessResult()
+
+ // The result must be observed immediately otherwise it won't execute (which will result in the
+ // language not being updated).
+ val resultProvider =
+ translationController.updateAppLanguage(PROFILE_ID_0, createAppLanguageSelection(ENGLISH))
+ val updateMonitor = monitorFactory.createMonitor(resultProvider)
+
+ updateMonitor.waitForNextSuccessResult()
+ langMonitor.ensureNextResultIsSuccess()
+ }
+
+ @Test
+ fun testGetAppLanguage_uninitialized_returnsSystemLanguage() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val languageProvider = translationController.getAppLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testGetAppLanguage_updateLanguageToEnglish_returnsEnglish() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, ENGLISH)
+
+ val languageProvider = translationController.getAppLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAppLanguage_updateLanguageToHindi_returnsHindi() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val languageProvider = translationController.getAppLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetAppLanguage_updateLanguageToUseSystem_returnsSystemLanguage() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+
+ val languageProvider = translationController.getAppLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAppLanguage_useSystemLang_updateLocale_notifiesProviderWithNewLanguage() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ val languageProvider = translationController.getAppLanguage(PROFILE_ID_0)
+ val monitor = monitorFactory.createMonitor(languageProvider)
+ monitor.waitForNextSuccessResult()
+
+ localeController.setAsDefault(createDisplayLocaleForLanguage(HINDI), Configuration())
+
+ // The language should be updated to English since the system language was changed.
+ val updatedLanguage = monitor.waitForNextSuccessResult()
+ assertThat(updatedLanguage).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetAppLanguage_updateLanguageToEnglish_differentProfile_returnsDifferentLang() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val languageProvider = translationController.getAppLanguage(PROFILE_ID_1)
+
+ // English is returned since the language is being fetched for a different profile.
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAppLanguageLocale_uninitialized_returnsLocaleWithSystemLanguage() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = translationController.getAppLanguageLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ val appStringId = context.languageDefinition.appStringId
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(appStringId.languageTypeCase).isEqualTo(IETF_BCP47_ID)
+ assertThat(appStringId.ietfBcp47Id.ietfLanguageTag).isEmpty()
+ assertThat(appStringId.androidResourcesLanguageId.languageCode).isEmpty()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testGetAppLanguageLocale_updateLanguageToEnglish_returnsEnglishLocale() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, ENGLISH)
+
+ val localeProvider = translationController.getAppLanguageLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ // This region comes from the default locale.
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testGetAppLanguageLocale_updateLanguageToPortuguese_returnsPortugueseLocale() {
+ forceDefaultLocale(BRAZIL_ENGLISH_LOCALE)
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, BRAZILIAN_PORTUGUESE)
+
+ val localeProvider = translationController.getAppLanguageLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ // This region comes from the default locale.
+ assertThat(context.regionDefinition.region).isEqualTo(BRAZIL)
+ }
+
+ @Test
+ fun testGetAppLanguageLocale_updateLanguageToUseSystem_returnsSystemLanguageLocale() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+
+ val localeProvider = translationController.getAppLanguageLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAppLanguageLocale_useSystemLang_updateLocale_notifiesProviderWithNewLocale() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ val localeProvider = translationController.getAppLanguageLocale(PROFILE_ID_0)
+ val monitor = monitorFactory.createMonitor(localeProvider)
+ monitor.waitForNextSuccessResult()
+
+ localeController.setAsDefault(createDisplayLocaleForLanguage(HINDI), Configuration())
+
+ // The language should be updated to English since the system language was changed.
+ val updateLocale = monitor.waitForNextSuccessResult()
+ val context = updateLocale.localeContext
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetAppLanguageLocale_updateLangToEnglish_differentProfile_returnsDifferentLocale() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val localeProvider = translationController.getAppLanguageLocale(PROFILE_ID_1)
+
+ // English is returned since the language is being fetched for a different profile.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(APP_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ }
+
+ /* Tests for written translation content functions */
+
+ @Test
+ fun testUpdateWrittenContentLanguage_returnsSuccess() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val resultProvider =
+ translationController.updateWrittenTranslationContentLanguage(
+ PROFILE_ID_0, createWrittenTranslationLanguageSelection(ENGLISH)
+ )
+
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ @Test
+ fun testUpdateWrittenContentLanguage_notifiesProviderWithChange() {
+ forceDefaultLocale(Locale.US)
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+ val langMonitor = monitorFactory.createMonitor(languageProvider)
+ langMonitor.waitForNextSuccessResult()
+
+ // The result must be observed immediately otherwise it won't execute (which will result in the
+ // language not being updated).
+ val resultProvider =
+ translationController.updateWrittenTranslationContentLanguage(
+ PROFILE_ID_0, createWrittenTranslationLanguageSelection(BRAZILIAN_PORTUGUESE)
+ )
+ val updateMonitor = monitorFactory.createMonitor(resultProvider)
+
+ updateMonitor.waitForNextSuccessResult()
+ langMonitor.ensureNextResultIsSuccess()
+ }
+
+ @Test
+ fun testGetWrittenContentLang_uninitialized_rootLocale_returnsFailure() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+
+ val error = monitorFactory.waitForNextFailureResult(languageProvider)
+ assertThat(error).hasMessageThat().contains("doesn't match supported language definitions")
+ }
+
+ @Test
+ fun testGetWrittenContentLang_uninitialized_englishLocale_returnsSystemLanguage() {
+ forceDefaultLocale(Locale.US)
+
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetWrittenContentLang_updateLanguageToEnglish_returnsEnglish() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, ENGLISH)
+
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetWrittenContentLang_updateLanguageToHindi_returnsHindi() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetWrittenContentLang_updateLanguageToUseApp_returnsAppLanguage() {
+ // First, initialize the language to Hindi before overwriting to use the app language.
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureWrittenTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetWrittenContentLang_useAppLang_updateAppLanguage_notifiesProviderWithNewLang() {
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureWrittenTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, BRAZILIAN_PORTUGUESE)
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+
+ // Changing the app language should change the provided language since this provider depends on
+ // the app strings language.
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ }
+
+ @Test
+ fun testGetWrittenContentLang_useSystemLangForApp_updateLocale_notifiesProviderWithNewLang() {
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureWrittenTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ localeController.setAsDefault(createDisplayLocaleForLanguage(HINDI), Configuration())
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_0)
+
+ // Changing the locale should change the language since this provider depends on the app strings
+ // language & app strings depend on the system locale.
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetWrittenContentLang_updateLanguageToEnglish_differentProfile_returnsDifferentLang() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val languageProvider = translationController.getWrittenTranslationContentLanguage(PROFILE_ID_1)
+
+ // English is returned since the language is being fetched for a different profile.
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_uninitialized_rootLocale_returnsFailure() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_0)
+
+ val error = monitorFactory.waitForNextFailureResult(localeProvider)
+ assertThat(error).hasMessageThat().contains("doesn't match supported language definitions")
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_uninitialized_englishLocale_returnsLocaleWithSystemLanguage() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(CONTENT_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.regionDefinition.region).isEqualTo(UNITED_STATES)
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_updateLanguageToEnglish_returnsEnglishLocale() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, ENGLISH)
+
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(CONTENT_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ // This region comes from the default locale.
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_updateLanguageToPortuguese_returnsPortugueseLocale() {
+ forceDefaultLocale(BRAZIL_ENGLISH_LOCALE)
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, BRAZILIAN_PORTUGUESE)
+
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(CONTENT_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ // This region comes from the default locale.
+ assertThat(context.regionDefinition.region).isEqualTo(BRAZIL)
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_updateLanguageToUseApp_returnsAppLanguage() {
+ // First, initialize the language to Hindi before overwriting to use the app language.
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureWrittenTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(CONTENT_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_useAppLang_updateAppLanguage_notifiesProviderWithNewLang() {
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureWrittenTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, BRAZILIAN_PORTUGUESE)
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_0)
+
+ // Changing the app language should change the provided language since this provider depends on
+ // the app strings language.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(CONTENT_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_useSystemLangForApp_updateLocale_notifiesProviderWithNewLang() {
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureWrittenTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ localeController.setAsDefault(createDisplayLocaleForLanguage(HINDI), Configuration())
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_0)
+
+ // Changing the locale should change the language since this provider depends on the app strings
+ // language & app strings depend on the system locale.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(CONTENT_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetWrittenContentLocale_updateLangToEnglish_differentProfile_returnsDifferentLocale() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureWrittenTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val localeProvider = translationController.getWrittenTranslationContentLocale(PROFILE_ID_1)
+
+ // English is returned since the language is being fetched for a different profile.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(CONTENT_STRINGS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ }
+
+ /* Tests for audio translation content functions */
+
+ @Test
+ fun testUpdateAudioLanguage_returnsSuccess() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val resultProvider =
+ translationController.updateAudioTranslationContentLanguage(
+ PROFILE_ID_0, createAudioTranslationLanguageSelection(ENGLISH)
+ )
+
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ @Test
+ fun testUpdateAudioLanguage_notifiesProviderWithChange() {
+ forceDefaultLocale(Locale.US)
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+ val langMonitor = monitorFactory.createMonitor(languageProvider)
+ langMonitor.waitForNextSuccessResult()
+
+ // The result must be observed immediately otherwise it won't execute (which will result in the
+ // language not being updated).
+ val resultProvider =
+ translationController.updateAudioTranslationContentLanguage(
+ PROFILE_ID_0, createAudioTranslationLanguageSelection(BRAZILIAN_PORTUGUESE)
+ )
+ val updateMonitor = monitorFactory.createMonitor(resultProvider)
+
+ updateMonitor.waitForNextSuccessResult()
+ langMonitor.ensureNextResultIsSuccess()
+ }
+
+ @Test
+ fun testGetAudioLanguage_uninitialized_rootLocale_returnsFailure() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+
+ val error = monitorFactory.waitForNextFailureResult(languageProvider)
+ assertThat(error).hasMessageThat().contains("doesn't match supported language definitions")
+ }
+
+ @Test
+ fun testGetAudioLanguage_uninitialized_englishLocale_returnsSystemLanguage() {
+ forceDefaultLocale(Locale.US)
+
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAudioLanguage_updateLanguageToEnglish_returnsEnglish() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, ENGLISH)
+
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAudioLanguage_updateLanguageToHindi_returnsHindi() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetAudioLanguage_updateLanguageToUseApp_returnsAppLanguage() {
+ // First, initialize the language to Hindi before overwriting to use the app language.
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureAudioTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAudioLanguage_useAppLang_updateAppLanguage_notifiesProviderWithNewLang() {
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureAudioTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, BRAZILIAN_PORTUGUESE)
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+
+ // Changing the app language should change the provided language since this provider depends on
+ // the app strings language.
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ }
+
+ @Test
+ fun testGetAudioLanguage_useSystemLangForApp_updateLocale_notifiesProviderWithNewLang() {
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureAudioTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ localeController.setAsDefault(createDisplayLocaleForLanguage(HINDI), Configuration())
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_0)
+
+ // Changing the locale should change the language since this provider depends on the app strings
+ // language & app strings depend on the system locale.
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetAudioLanguage_updateLanguageToEnglish_differentProfile_returnsDifferentLang() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val languageProvider = translationController.getAudioTranslationContentLanguage(PROFILE_ID_1)
+
+ // English is returned since the language is being fetched for a different profile.
+ val language = monitorFactory.waitForNextSuccessfulResult(languageProvider)
+ assertThat(language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAudioLocale_uninitialized_rootLocale_returnsFailure() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_0)
+
+ val error = monitorFactory.waitForNextFailureResult(localeProvider)
+ assertThat(error).hasMessageThat().contains("doesn't match supported language definitions")
+ }
+
+ @Test
+ fun testGetAudioLocale_uninitialized_englishLocale_returnsLocaleWithSystemLanguage() {
+ forceDefaultLocale(Locale.US)
+
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(AUDIO_TRANSLATIONS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ assertThat(context.regionDefinition.region).isEqualTo(UNITED_STATES)
+ }
+
+ @Test
+ fun testGetAudioLocale_updateLanguageToEnglish_returnsEnglishLocale() {
+ forceDefaultLocale(Locale.ROOT)
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, ENGLISH)
+
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(AUDIO_TRANSLATIONS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ // This region comes from the default locale.
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ }
+
+ @Test
+ fun testGetAudioLocale_updateLanguageToPortuguese_returnsPortugueseLocale() {
+ forceDefaultLocale(BRAZIL_ENGLISH_LOCALE)
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, BRAZILIAN_PORTUGUESE)
+
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(AUDIO_TRANSLATIONS)
+ assertThat(context.languageDefinition.language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ // This region comes from the default locale.
+ assertThat(context.regionDefinition.region).isEqualTo(BRAZIL)
+ }
+
+ @Test
+ fun testGetAudioLocale_updateLanguageToUseApp_returnsAppLanguage() {
+ // First, initialize the language to Hindi before overwriting to use the app language.
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureAudioTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_0)
+
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(AUDIO_TRANSLATIONS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ }
+
+ @Test
+ fun testGetAudioLocale_useAppLang_updateAppLanguage_notifiesProviderWithNewLang() {
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureAudioTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ ensureAppLanguageIsUpdatedTo(PROFILE_ID_0, BRAZILIAN_PORTUGUESE)
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_0)
+
+ // Changing the app language should change the provided language since this provider depends on
+ // the app strings language.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(AUDIO_TRANSLATIONS)
+ assertThat(context.languageDefinition.language).isEqualTo(BRAZILIAN_PORTUGUESE)
+ }
+
+ @Test
+ fun testGetAudioLocale_useSystemLangForApp_updateLocale_notifiesProviderWithNewLang() {
+ ensureAppLanguageIsUpdatedToUseSystem(PROFILE_ID_0)
+ forceDefaultLocale(Locale.US)
+ ensureAudioTranslationsLanguageIsUpdatedToUseApp(PROFILE_ID_0)
+
+ localeController.setAsDefault(createDisplayLocaleForLanguage(HINDI), Configuration())
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_0)
+
+ // Changing the locale should change the language since this provider depends on the app strings
+ // language & app strings depend on the system locale.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(AUDIO_TRANSLATIONS)
+ assertThat(context.languageDefinition.language).isEqualTo(HINDI)
+ }
+
+ @Test
+ fun testGetAudioLocale_updateLangToEnglish_differentProfile_returnsDifferentLocale() {
+ forceDefaultLocale(Locale.ENGLISH)
+ ensureAudioTranslationsLanguageIsUpdatedTo(PROFILE_ID_0, HINDI)
+
+ val localeProvider = translationController.getAudioTranslationContentLocale(PROFILE_ID_1)
+
+ // English is returned since the language is being fetched for a different profile.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ assertThat(context.usageMode).isEqualTo(AUDIO_TRANSLATIONS)
+ assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
+ }
+
+ /* Tests for string extraction functions */
+
+ @Test
+ fun testExtractString_defaultSubtitledHtml_defaultContext_returnsEmptyString() {
+ val extracted =
+ translationController.extractString(
+ SubtitledHtml.getDefaultInstance(), WrittenTranslationContext.getDefaultInstance()
+ )
+
+ assertThat(extracted).isEmpty()
+ }
+
+ @Test
+ fun testExtractString_defaultSubtitledHtml_validContext_returnsEmptyString() {
+ val context = WrittenTranslationContext.newBuilder().apply {
+ putTranslations(
+ "other_content_id",
+ Translation.newBuilder().apply {
+ html = "Translated string"
+ }.build()
+ )
+ }.build()
+
+ val extracted = translationController.extractString(SubtitledHtml.getDefaultInstance(), context)
+
+ assertThat(extracted).isEmpty()
+ }
+
+ @Test
+ fun testExtractString_subtitledHtml_defaultContext_returnsUntranslatedHtml() {
+ val subtitledHtml = SubtitledHtml.newBuilder().apply {
+ contentId = "content_id"
+ html = "default html"
+ }.build()
+
+ val extracted =
+ translationController.extractString(
+ subtitledHtml, WrittenTranslationContext.getDefaultInstance()
+ )
+
+ assertThat(extracted).isEqualTo("default html")
+ }
+
+ @Test
+ fun testExtractString_subtitledHtml_validContext_missingContentId_returnsUntranslatedHtml() {
+ val subtitledHtml = SubtitledHtml.newBuilder().apply {
+ contentId = "content_id"
+ html = "default html"
+ }.build()
+ val context = WrittenTranslationContext.newBuilder().apply {
+ putTranslations(
+ "other_content_id",
+ Translation.newBuilder().apply {
+ html = "Translated string"
+ }.build()
+ )
+ }.build()
+
+ val extracted = translationController.extractString(subtitledHtml, context)
+
+ // The content ID doesn't match in the context.
+ assertThat(extracted).isEqualTo("default html")
+ }
+
+ @Test
+ fun testExtractString_subtitledHtml_validContext_includesContentId_returnsTranslatedHtml() {
+ val subtitledHtml = SubtitledHtml.newBuilder().apply {
+ contentId = "content_id"
+ html = "default html"
+ }.build()
+ val context = WrittenTranslationContext.newBuilder().apply {
+ putTranslations(
+ "content_id",
+ Translation.newBuilder().apply {
+ html = "Translated string"
+ }.build()
+ )
+ }.build()
+
+ val extracted = translationController.extractString(subtitledHtml, context)
+
+ // The context ID does match, so the matching string is extracted.
+ assertThat(extracted).isEqualTo("Translated string")
+ }
+
+ @Test
+ fun testExtractString_defaultSubtitledUnicode_defaultContext_returnsEmptyString() {
+ val extracted =
+ translationController.extractString(
+ SubtitledUnicode.getDefaultInstance(), WrittenTranslationContext.getDefaultInstance()
+ )
+
+ assertThat(extracted).isEmpty()
+ }
+
+ @Test
+ fun testExtractString_defaultSubtitledUnicode_validContext_returnsEmptyString() {
+ val context = WrittenTranslationContext.newBuilder().apply {
+ putTranslations(
+ "other_content_id",
+ Translation.newBuilder().apply {
+ html = "Translated string"
+ }.build()
+ )
+ }.build()
+
+ val extracted =
+ translationController.extractString(SubtitledUnicode.getDefaultInstance(), context)
+
+ assertThat(extracted).isEmpty()
+ }
+
+ @Test
+ fun testExtractString_subtitledUnicode_defaultContext_returnsUntranslatedUnicode() {
+ val subtitledUnicode = SubtitledUnicode.newBuilder().apply {
+ contentId = "content_id"
+ unicodeStr = "default str"
+ }.build()
+
+ val extracted =
+ translationController.extractString(
+ subtitledUnicode, WrittenTranslationContext.getDefaultInstance()
+ )
+
+ assertThat(extracted).isEqualTo("default str")
+ }
+
+ @Test
+ fun testExtractString_subtitledUnicode_validContext_missingContentId_returnsUnxlatedUnicode() {
+ val subtitledUnicode = SubtitledUnicode.newBuilder().apply {
+ contentId = "content_id"
+ unicodeStr = "default str"
+ }.build()
+ val context = WrittenTranslationContext.newBuilder().apply {
+ putTranslations(
+ "other_content_id",
+ Translation.newBuilder().apply {
+ html = "Translated string"
+ }.build()
+ )
+ }.build()
+
+ val extracted = translationController.extractString(subtitledUnicode, context)
+
+ // The content ID doesn't match in the context.
+ assertThat(extracted).isEqualTo("default str")
+ }
+
+ @Test
+ fun testExtractString_subtitledUnicode_validContext_includesContentId_returnsTranslatedUnicode() {
+ val subtitledUnicode = SubtitledUnicode.newBuilder().apply {
+ contentId = "content_id"
+ unicodeStr = "default str"
+ }.build()
+ val context = WrittenTranslationContext.newBuilder().apply {
+ putTranslations(
+ "content_id",
+ Translation.newBuilder().apply {
+ html = "Translated string"
+ }.build()
+ )
+ }.build()
+
+ val extracted = translationController.extractString(subtitledUnicode, context)
+
+ // The context ID does match, so the matching string is extracted.
+ assertThat(extracted).isEqualTo("Translated string")
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ private fun forceDefaultLocale(locale: Locale) {
+ context.applicationContext.resources.configuration.setLocale(locale)
+ Locale.setDefault(locale)
+ }
+
+ private fun createDisplayLocaleForLanguage(language: OppiaLanguage): OppiaLocale.DisplayLocale {
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(language)
+ return monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ }
+
+ private fun ensureAppLanguageIsUpdatedToUseSystem(profileId: ProfileId) {
+ val resultProvider =
+ translationController.updateAppLanguage(profileId, APP_LANGUAGE_SELECTION_SYSTEM)
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ private fun ensureAppLanguageIsUpdatedTo(profileId: ProfileId, language: OppiaLanguage) {
+ val resultProvider =
+ translationController.updateAppLanguage(profileId, createAppLanguageSelection(language))
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ private fun createAppLanguageSelection(language: OppiaLanguage): AppLanguageSelection {
+ return AppLanguageSelection.newBuilder().apply {
+ selectedLanguage = language
+ }.build()
+ }
+
+ private fun ensureWrittenTranslationsLanguageIsUpdatedTo(
+ profileId: ProfileId,
+ language: OppiaLanguage
+ ) {
+ val resultProvider =
+ translationController.updateWrittenTranslationContentLanguage(
+ profileId, createWrittenTranslationLanguageSelection(language)
+ )
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ private fun ensureWrittenTranslationsLanguageIsUpdatedToUseApp(profileId: ProfileId) {
+ val resultProvider =
+ translationController.updateWrittenTranslationContentLanguage(
+ profileId, WRITTEN_TRANSLATION_LANGUAGE_SELECTION_APP_LANGUAGE
+ )
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ private fun createWrittenTranslationLanguageSelection(
+ language: OppiaLanguage
+ ): WrittenTranslationLanguageSelection {
+ return WrittenTranslationLanguageSelection.newBuilder().apply {
+ selectedLanguage = language
+ }.build()
+ }
+
+ private fun ensureAudioTranslationsLanguageIsUpdatedTo(
+ profileId: ProfileId,
+ language: OppiaLanguage
+ ) {
+ val resultProvider =
+ translationController.updateAudioTranslationContentLanguage(
+ profileId, createAudioTranslationLanguageSelection(language)
+ )
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ private fun ensureAudioTranslationsLanguageIsUpdatedToUseApp(profileId: ProfileId) {
+ val resultProvider =
+ translationController.updateAudioTranslationContentLanguage(
+ profileId, AUDIO_TRANSLATION_LANGUAGE_SELECTION_APP_LANGUAGE
+ )
+ monitorFactory.waitForNextSuccessfulResult(resultProvider)
+ }
+
+ private fun createAudioTranslationLanguageSelection(
+ language: OppiaLanguage
+ ): AudioTranslationLanguageSelection {
+ return AudioTranslationLanguageSelection.newBuilder().apply {
+ selectedLanguage = language
+ }.build()
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LogStorageModule::class, NetworkConnectionUtilDebugModule::class,
+ TestLogReportingModule::class, LoggerModule::class, TestDispatcherModule::class,
+ LocaleProdModule::class, FakeOppiaClockModule::class, RobolectricModule::class,
+ AssetModule::class
+ ]
+ )
+ interface TestApplicationComponent : DataProvidersInjector {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(translationControllerTest: TranslationControllerTest)
+ }
+
+ class TestApplication : Application(), DataProvidersInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerTranslationControllerTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build()
+ }
+
+ fun inject(translationControllerTest: TranslationControllerTest) {
+ component.inject(translationControllerTest)
+ }
+
+ override fun getDataProvidersInjector(): DataProvidersInjector = component
+ }
+
+ private companion object {
+ private val BRAZIL_ENGLISH_LOCALE = Locale("en", "BR")
+
+ private val PROFILE_ID_0 = ProfileId.newBuilder().apply {
+ internalId = 0
+ }.build()
+
+ private val PROFILE_ID_1 = ProfileId.newBuilder().apply {
+ internalId = 1
+ }.build()
+
+ private val APP_LANGUAGE_SELECTION_SYSTEM = AppLanguageSelection.newBuilder().apply {
+ useSystemLanguageOrAppDefault = true
+ }.build()
+
+ private val WRITTEN_TRANSLATION_LANGUAGE_SELECTION_APP_LANGUAGE =
+ WrittenTranslationLanguageSelection.newBuilder().apply {
+ useAppLanguage = true
+ }.build()
+
+ private val AUDIO_TRANSLATION_LANGUAGE_SELECTION_APP_LANGUAGE =
+ AudioTranslationLanguageSelection.newBuilder().apply {
+ useAppLanguage = true
+ }.build()
+ }
+}
diff --git a/domain/src/test/java/org/oppia/android/domain/util/StateRetrieverTest.kt b/domain/src/test/java/org/oppia/android/domain/util/StateRetrieverTest.kt
index 510787d58b6..a747f675122 100644
--- a/domain/src/test/java/org/oppia/android/domain/util/StateRetrieverTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/util/StateRetrieverTest.kt
@@ -23,6 +23,7 @@ import org.oppia.android.app.model.TranslatableHtmlContentId
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.logging.EnableConsoleLog
import org.oppia.android.util.logging.EnableFileLog
@@ -427,9 +428,8 @@ class StateRetrieverTest {
@Singleton
@Component(
modules = [
- TestModule::class,
- TestDispatcherModule::class,
- RobolectricModule::class, FakeOppiaClockModule::class
+ TestModule::class, TestDispatcherModule::class, RobolectricModule::class,
+ FakeOppiaClockModule::class, AssetModule::class
]
)
interface TestApplicationComponent {
diff --git a/domain/src/test/res/values/strings.xml b/domain/src/test/res/values/strings.xml
new file mode 100644
index 00000000000..97f8a64e96e
--- /dev/null
+++ b/domain/src/test/res/values/strings.xml
@@ -0,0 +1,27 @@
+
+
+
+ Basic string
+ Basic string2
+ האם התכוונת ל %s
+
+ -
+ 1 item
+
+ -
+ 2 items
+
+
+
+ -
+ 1 item
+
+ -
+ %s items
+
+
+
+ - @string/test_basic_string
+ - @string/test_basic_string2
+
+
diff --git a/instrumentation/src/java/org/oppia/android/instrumentation/application/BUILD.bazel b/instrumentation/src/java/org/oppia/android/instrumentation/application/BUILD.bazel
index b65fba2144f..4a14c9cf370 100644
--- a/instrumentation/src/java/org/oppia/android/instrumentation/application/BUILD.bazel
+++ b/instrumentation/src/java/org/oppia/android/instrumentation/application/BUILD.bazel
@@ -22,7 +22,7 @@ kt_android_library(
"//data/src/main/java/org/oppia/android/data/backends/gae:network_config_annotations",
"//domain",
"//utility",
- "//utility/src/main/java/org/oppia/android/util/caching:caching_module",
+ "//utility/src/main/java/org/oppia/android/util/caching:caching_prod_module",
],
)
diff --git a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt
index dacf066c0bc..38370de9e23 100644
--- a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt
+++ b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt
@@ -39,6 +39,7 @@ import org.oppia.android.domain.question.QuestionModule
import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
import org.oppia.android.util.accessibility.AccessibilityProdModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CachingModule
import org.oppia.android.util.logging.LoggerModule
import org.oppia.android.util.logging.firebase.DebugLogReportingModule
@@ -80,7 +81,7 @@ import javax.inject.Singleton
PlatformParameterModule::class, ExplorationStorageModule::class,
DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class,
- EndToEndTestNetworkConfigModule::class,
+ EndToEndTestNetworkConfigModule::class, AssetModule::class,
// TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then
// directly exclude debug files from the build and thus won't be requiring this module.
NetworkConnectionDebugUtilModule::class
diff --git a/model/BUILD.bazel b/model/BUILD.bazel
index 7c805314de7..d64d7e2a340 100644
--- a/model/BUILD.bazel
+++ b/model/BUILD.bazel
@@ -84,6 +84,18 @@ java_lite_proto_library(
deps = [":interaction_object_proto"],
)
+proto_library(
+ name = "languages_proto",
+ srcs = ["src/main/proto/languages.proto"],
+ visibility = ["//visibility:public"],
+)
+
+java_lite_proto_library(
+ name = "languages_java_proto_lite",
+ visibility = ["//visibility:public"],
+ deps = [":languages_proto"],
+)
+
proto_library(
name = "onboarding_proto",
srcs = ["src/main/proto/onboarding.proto"],
@@ -113,6 +125,7 @@ proto_library(
java_lite_proto_library(
name = "subtitled_html_java_proto_lite",
+ visibility = ["//:oppia_api_visibility"],
deps = [":subtitled_html_proto"],
)
@@ -123,6 +136,7 @@ proto_library(
java_lite_proto_library(
name = "subtitled_unicode_java_proto_lite",
+ visibility = ["//:oppia_api_visibility"],
deps = [":subtitled_unicode_proto"],
)
diff --git a/model/build.gradle b/model/build.gradle
index 5ba79d77c05..5030ef8bebf 100644
--- a/model/build.gradle
+++ b/model/build.gradle
@@ -3,36 +3,28 @@ apply plugin: 'com.google.protobuf'
protobuf {
protoc {
- artifact = 'com.google.protobuf:protoc:3.0.0'
- }
- plugins {
- javalite {
- artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
- }
+ artifact = 'com.google.protobuf:protoc:3.8.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
- remove java
- }
- task.plugins {
- javalite {}
+ java {
+ // This setup is per https://github.com/google/protobuf-gradle-plugin/issues/315.
+ option 'lite'
+ }
}
}
}
}
dependencies {
- // This must use compile in order for lite protobufs to generate correctly.
- // TODO(#59): Move to Bazel to avoid relying on a deprecated Gradle directive.
- compile 'com.google.protobuf:protobuf-lite:3.0.0'
+ implementation 'com.google.protobuf:protobuf-javalite:3.17.3'
}
sourceSets {
- main.java.srcDirs += "${protobuf.generatedFilesBaseDir}/main/javalite"
- main.java.srcDirs += "$projectDir/src/main/proto"
-}
-
-processResources {
- exclude('**/*.proto')
+ main {
+ proto {
+ srcDir 'src/main/proto'
+ }
+ }
}
diff --git a/model/src/main/proto/languages.proto b/model/src/main/proto/languages.proto
new file mode 100644
index 00000000000..c992db49407
--- /dev/null
+++ b/model/src/main/proto/languages.proto
@@ -0,0 +1,265 @@
+syntax = "proto3";
+
+package model;
+
+option java_package = "org.oppia.android.app.model";
+option java_multiple_files = true;
+
+// The list of languages partly or fully supported natively by the Android app.
+enum OppiaLanguage {
+ // Corresponds to an unspecified, unknown, or unsupported language.
+ LANGUAGE_UNSPECIFIED = 0;
+
+ // Corresponds to the Arabic (اَلْعَرَبِيَّةُ) macro language. IETF BCP 47 language tag: ar.
+ ARABIC = 1;
+
+ // Corresponds to the English (English) macro language. IETF BCP 47 language tag: en.
+ ENGLISH = 2;
+
+ // Corresponds to the Hindi (हिन्दी) macro language. IETF BCP 47 language tag: hi.
+ HINDI = 3;
+
+ // Corresponds to the Hindi-English macaraonic language. Custom language tag: hi-en.
+ HINGLISH = 4;
+
+ // Corresponds to the Portuguese (português) macro language. IETF BCP 47 language tag: pt.
+ PORTUGUESE = 5;
+
+ // Corresponds to the Brazilian variant of Portuguese. IETF BCP 47 language tag: pt-BR.
+ BRAZILIAN_PORTUGUESE = 6;
+}
+
+// The list of regions explicitly supported natively by the Android app. Note that the app is not
+// automatically unsupported in countries not on this list. Countries absent from this list may
+// default to default system behavior for certain localization situations (such as date & time
+// formatting) rather than being explicitly handled by the app.
+//
+// Note also that these regions cannot be construed as countries, nor as the area the user is in.
+// The system's locale can be changed by the user, so this is a best-effort basis to match to the
+// user's current system. Further, the app retains future support for regions not directly supported
+// by the Android system.
+enum OppiaRegion {
+ // Corresponds to an unspecified, unknown, or undefined region. In these cases, the app will rely
+ // on system behavior for locale-related decisions (such as formatting).
+ REGION_UNSPECIFIED = 0;
+
+ // Corresponds to Brazil (Brasil). IETF BCP 47 region tag: BR.
+ BRAZIL = 1;
+
+ // Corresponds to India (Bhārat Gaṇarājya). IETF BCP 47 region tag: IN.
+ INDIA = 2;
+
+ // Corresponds to United State of America (U.S.A.). IETF BCP 47 region tag: US.
+ UNITED_STATES = 3;
+}
+
+// Defines the list of supported languages in the app.
+message SupportedLanguages {
+ // The list of language definitions, one for each language. If any languages are not represented
+ // in this list then it's assumed that they are not supported. If a language is represented
+ // multiple times, the first occurrence in the list for that language is used.
+ repeated LanguageSupportDefinition language_definitions = 1;
+}
+
+// Defines the list of supported regions in the app. Note that countries missing from this list are
+// handled generically (that is, they rely on the system to handle certain localization contexts
+// or otherwise fallback to the default locale of the system). Note that regions are only associated
+// with languages for the purpose of localizing text such as date & time formats.
+message SupportedRegions {
+ // The list of region deifnitions, one for each OppiaRegion. If any regions are not represented in
+ // this list then the app relies on default behavior for that region's corresponding locale. If a
+ // region is represented multiply times, the first occurrence in the list for that region is used.
+ repeated RegionSupportDefinition region_definitions = 1;
+}
+
+// Defines the support for a specific language.
+message LanguageSupportDefinition {
+ // The language corresponding to this definition.
+ OppiaLanguage language = 1;
+
+ // The macro language to fall back to if this language is not supported on the local system or
+ // for specific content (e.g.: 'pt' could be a fallback for Brazilian Portuguese).
+ OppiaLanguage fallback_macro_language = 2;
+
+ // Corresponds to the minimum SDK version required to natively support this language. May be
+ // missing (i.e. 0) if the language is not natively supported by Android. Note that Android does
+ // not, strictly speaking, support individual languages at the SDK level since the actual support
+ // for languages may depend on the specific OEM and installation situation. However, Android does
+ // support certain versions of the Unicode standard based on the release which indicates which
+ // characters it _can_ support for rendering. That means this field indicates the minimum version
+ // of Android needed to render this language but not necessarily have Locale support for it. Note
+ // also that Android always allows defining translation strings for language codes that it does
+ // not recognize/natively support.
+ //
+ // https://developer.android.com/guide/topics/resources/internationalization provides a reference
+ // for Android Unicode support. Note that Android 7.0 and above leverages the ICU4J library for
+ // handling newer Unicode standards and is thus less clear about compatibility. The app doesn't
+ // currently compile in ICU4J.
+ //
+ // https://unicode.org/standard/supported.html provides a reference for which scripts are
+ // supported by each Unicode version. The IANA registry for languages includes scripts:
+ // http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry. Languages
+ // also have their scripts defined, so a mapping can be established between language and minimum
+ // Android SDK version.
+ int32 min_android_sdk_version = 3;
+
+ // Details of how to identify this language when translating app strings. If missing, this
+ // language will be unsupported for app strings.
+ LanguageId app_string_id = 4;
+
+ // Details of how to identify this language when translating content strings. If missing, this
+ // language will be unsupported for content strings.
+ LanguageId content_string_id = 5;
+
+ // Details of how to identify this language when selecting audio voiceover translations. If
+ // missing, this language will be unsupported for audio voiceovers.
+ LanguageId audio_translation_id = 6;
+
+ // A representation of identifying a particular language in different contexts.
+ message LanguageId {
+ // Corresponds to the type of language being identified.
+ oneof language_type {
+ // Indicates that this identifier corresponds to an IETF BCP 47 identified language.
+ IetfBcp47LanguageId ietf_bcp47_id = 1;
+
+ // Indicates that this identifier corresponds to a macaronic language.
+ MacaronicLanguageId macaronic_id = 2;
+ }
+
+ // Identifier for retrieving Android resources corresponding to this language. If this is absent
+ // then it's assumed there are no Android resources corresponding to this language.
+ AndroidLanguageId android_resources_language_id = 3;
+ }
+
+ // An identifier representation for IETF BCP 47 languages. Note that ISO 639-1/2/3 is not used
+ // since it can't represent regional languages like Canadian French. See the following article for
+ // details on how IETF language tags are formed: https://www.unfoldingword.org/ietf. Current tag
+ // registry: http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry.
+ // Note that the list above will contain languages not supported on all Android platforms.
+ message IetfBcp47LanguageId {
+ // The language tag according to the IETF BCP 47 standard.
+ string ietf_language_tag = 1;
+ }
+
+ // An identifier representation for macaronic languages (which are languages which combine two
+ // others, e.g.: Hinglish).
+ message MacaronicLanguageId {
+ // The combined language code for this macaronic language (e.g. 'hi-en' for Hinglish). Note that
+ // the constituent parts of the language may not necessarily correspond to ISO 639-1 language
+ // codes. It's also expected that order matters here: hi-en and en_hi would not correspond to
+ // the same macaronic language.
+ string combined_language_code = 1;
+ }
+
+ // An identifier representation for languages supported on Android via resource directories. Note
+ // that these may not exactly match with IETF BCP 47 since Android uses a slightly different
+ // system, and custom types are allowed by providing fake language/region codes (such as for
+ // hi-en).
+ message AndroidLanguageId {
+ // The language code understood by Android (usually an ISO 639-1 code, but custom codes may be
+ // used). This is always expected to be present for valid language IDs.
+ string language_code = 1;
+
+ // The language code understood by Android (usually an ISO 3166 alpha-2 code, but custom codes
+ // may be used). This may be absent when referencing macro languages.
+ string region_code = 2;
+ }
+}
+
+// Defines the support for a specific region.
+message RegionSupportDefinition {
+ // The specific region corresponding to this definition.
+ OppiaRegion region = 1;
+
+ // The IETF BCP 47 identifier corresponding to the region this region represents.
+ IetfBcp47RegionId region_id = 2;
+
+ // The list of languages corresponding to this region. Note that the app first prioritizes the
+ // selected language by the user (either via the Android system or through a language picker) when
+ // deciding which language to represent this region. If the user's locale and language selection
+ // do not match the region support definitions, the first language of this list will be used. If
+ // no languages are defined, the app will fall back to rely on the system's default behavior for
+ // the user's locale (if the system supports it, otherwise the default locale will be used).
+ repeated OppiaLanguage languages = 3;
+
+ // An identifier for IETF BCP 47 regions. The current registry of available regions is at:
+ // http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry (search for
+ // 'Type: region').
+ message IetfBcp47RegionId {
+ // The region tag according to the IETF BCP 47 standard, usually either an ISO 3166 alpha-2
+ // country code or a UN M.49 numeric-3 area code.
+ string ietf_region_tag = 1;
+ }
+}
+
+// Corresponds to a serializable context that can be used to reconstitute an OppiaLocale.
+message OppiaLocaleContext {
+ // The definition corresponding to this context's language.
+ LanguageSupportDefinition language_definition = 1;
+
+ // The definition corresponding to this context's fallback language.
+ LanguageSupportDefinition fallback_language_definition = 2;
+
+ // The definition corresponding to this context's region.
+ RegionSupportDefinition region_definition = 3;
+
+ // Indicates how the language & fallback support definitions should be used in this context. Note
+ // that this actually implies this context should not be used for other cases not indicated by
+ // this usage mode.
+ LanguageUsageMode usage_mode = 4;
+
+ // Corresponds to different usage modes that language definitions can be used in.
+ enum LanguageUsageMode {
+ // Indicates that no usage mode is defined (meaning that no aspects of language definitions
+ // should be used).
+ USAGE_MODE_UNSPECIFIED = 0;
+
+ // Indicates that the language definitions should only be used for retrieving app strings.
+ APP_STRINGS = 1;
+
+ // Indicates that the language definitions should only be used for translating content strings.
+ CONTENT_STRINGS = 2;
+
+ // Indicates that the language definitions should only be used for managing audio translations.
+ AUDIO_TRANSLATIONS = 3;
+ }
+}
+
+// Represents the selection of an app language.
+message AppLanguageSelection {
+ // Different types of selections that can be made when choosing an app language.
+ oneof selection_type {
+ // Indicates that the system-specified language should be used for app translations. The actual
+ // boolean value specified here doesn't matter.
+ bool use_system_language_or_app_default = 1;
+
+ // Indicates that the specified language should be used for app string translations.
+ OppiaLanguage selected_language = 2;
+ }
+}
+
+// Represents the selection of a written content translation language.
+message WrittenTranslationLanguageSelection {
+ // Different types of selections that can be made when choosing a language for written content
+ // translations. If this is not defined, the selection is assumed to be use_app_language.
+ oneof selection_type {
+ // Indicates that the selected app language should be used for written content translations.
+ bool use_app_language = 1;
+
+ // Indicates that the specified language should be used for written content translations.
+ OppiaLanguage selected_language = 2;
+ }
+}
+
+// Represents the selection of an audio voiceover language.
+message AudioTranslationLanguageSelection {
+ // Different types of selections that can be made when choosing a language for audio voiceovers.
+ // If this is not defined, the selection is assumed to be use_app_language.
+ oneof selection_type {
+ // Indicates that the selected app language should be used for audio voiceovers.
+ bool use_app_language = 1;
+
+ // Indicates that the specified language should be used for audio voiceovers.
+ OppiaLanguage selected_language = 2;
+ }
+}
diff --git a/model/src/main/proto/translation.proto b/model/src/main/proto/translation.proto
index aae405c34d0..09a28b74732 100644
--- a/model/src/main/proto/translation.proto
+++ b/model/src/main/proto/translation.proto
@@ -16,3 +16,9 @@ message Translation {
string html = 1;
bool needs_update = 2;
}
+
+// Represents the context for translating written content strings.
+message WrittenTranslationContext {
+ // A map from content ID to translation.
+ map translations = 1;
+}
diff --git a/oppia_android_application.bzl b/oppia_android_application.bzl
index 8c17e56cbec..870642a617a 100644
--- a/oppia_android_application.bzl
+++ b/oppia_android_application.bzl
@@ -232,23 +232,27 @@ def oppia_android_application(name, config_file, **kwargs):
module_zip_name = "%s_module_zip" % name
native.android_binary(
name = binary_name,
+ tags = ["manual"],
**kwargs
)
_convert_apk_to_module_aab(
name = module_aab_name,
input_file = ":%s.apk" % binary_name,
output_file = "%s.aab" % module_aab_name,
+ tags = ["manual"],
)
_convert_module_aab_to_structured_zip(
name = module_zip_name,
input_file = ":%s.aab" % module_aab_name,
output_file = "%s.zip" % module_zip_name,
+ tags = ["manual"],
)
_bundle_module_zip_into_deployable_aab(
name = name,
input_file = ":%s.zip" % module_zip_name,
config_file = config_file,
output_file = "%s.aab" % name,
+ tags = ["manual"],
)
def declare_deployable_application(name, aab_target):
@@ -275,4 +279,5 @@ def declare_deployable_application(name, aab_target):
_generate_apks_and_install(
name = name,
input_file = aab_target,
+ tags = ["manual"],
)
diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto
index 0354796382a..c9669b41a16 100644
--- a/scripts/assets/file_content_validation_checks.textproto
+++ b/scripts/assets/file_content_validation_checks.textproto
@@ -87,7 +87,6 @@ file_content_checks {
file_path_regex: "app/src/main/res/values/strings.xml"
prohibited_content_regex: "translatable=\"false\""
failure_message: "Untranslatable strings should go in untranslated_strings.xml, instead."
- exempted_file_name: "app/src/main/res/values/untranslated_strings.xml"
}
file_content_checks {
file_path_regex: ".+?.xml"
@@ -95,3 +94,18 @@ file_content_checks {
failure_message: "All strings outside strings.xml must be marked as not translatable, or moved to strings.xml."
exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml"
}
+file_content_checks {
+ file_path_regex: ".+?.xml"
+ prohibited_content_regex: ""
+ failure_message: "All plurals outside strings.xml must be marked as not translatable, or moved to strings.xml."
+ exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml"
+}
+file_content_checks {
+ file_path_regex: ".+?.kt"
+ prohibited_content_regex: "android.text.BidiFormatter"
+ failure_message: "Do not use Android's BidiFormatter directly. Instead, use the wrapper utility OppiaBidiFormatter so that tests can verify that formatting actually occurs on select strings."
+ exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt"
+ exempted_file_name: "testing/src/main/java/org/oppia/android/testing/robolectric/ShadowBidiFormatter.kt"
+ exempted_file_name: "testing/src/test/java/org/oppia/android/testing/robolectric/ShadowBidiFormatterTest.kt"
+ exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatterImpl.kt"
+}
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index edb70a7eeca..a4acde2b959 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -653,6 +653,7 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/
exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/AccessibilityTestModule.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/accessibility/FakeAccessibilityChecker.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/AssetRepository.kt"
+exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/AssetRepositoryImpl.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/CacheAssetsLocally.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/CachingModule.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/caching/LoadImagesFromAssets.kt"
@@ -666,6 +667,8 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/data/DataProvi
exempted_file_path: "utility/src/main/java/org/oppia/android/util/extensions/ContextExtensions.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/gcsresource/GcsResourceAnnotations.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/gcsresource/GcsResourceModule.kt"
+exempted_file_path: "utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatter.kt"
+exempted_file_path: "utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/ConsoleLogger.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/EventLogger.kt"
exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/ExceptionLogger.kt"
diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
index 675ebe06f53..9cf2871dd1a 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
@@ -58,6 +58,12 @@ class RegexPatternValidationCheckTest {
"Untranslatable strings should go in untranslated_strings.xml, instead."
private val translatableStringsGoInMainFileErrorMessage =
"All strings outside strings.xml must be marked as not translatable, or moved to strings.xml."
+ private val translatablePluralsGoInMainFileErrorMessage =
+ "All plurals outside strings.xml must be marked as not translatable, or moved to strings.xml."
+ private val importingAndroidBidiFormatterErrorMessage =
+ "Do not use Android's BidiFormatter directly. Instead, use the wrapper utility" +
+ " OppiaBidiFormatter so that tests can verify that formatting actually occurs on select" +
+ " strings."
private val wikiReferenceNote =
"Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" +
"#regexpatternvalidation-check for more details on how to fix this."
@@ -871,6 +877,48 @@ class RegexPatternValidationCheckTest {
)
}
+ @Test
+ fun testFileContent_translatablePlural_outsidePrimaryStringsFile_fileContentIsNotCorrect() {
+ val prohibitedContent = ""
+ tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values")
+ val stringFilePath = "app/src/main/res/values/untranslated_strings.xml"
+ tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent)
+
+ val exception = assertThrows(Exception::class) {
+ runScript()
+ }
+
+ assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR)
+ assertThat(outContent.toString().trim())
+ .isEqualTo(
+ """
+ $stringFilePath:1: $translatablePluralsGoInMainFileErrorMessage
+ $wikiReferenceNote
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testFileContent_bidiFormatterImport_fileContentIsNotCorrect() {
+ val prohibitedContent = "import android.text.BidiFormatter"
+ tempFolder.newFolder("testfiles", "data", "src", "main")
+ val stringFilePath = "data/src/main/SomeController.kt"
+ tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent)
+
+ val exception = assertThrows(Exception::class) {
+ runScript()
+ }
+
+ assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR)
+ assertThat(outContent.toString().trim())
+ .isEqualTo(
+ """
+ $stringFilePath:1: $importingAndroidBidiFormatterErrorMessage
+ $wikiReferenceNote
+ """.trimIndent()
+ )
+ }
+
@Test
fun testFileContent_untranslatableString_outsidePrimaryStringsFile_fileContentIsCorrect() {
val prohibitedContent = "Translatable"
diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel
index c57d9e81d61..ba3984493d8 100644
--- a/testing/BUILD.bazel
+++ b/testing/BUILD.bazel
@@ -89,6 +89,7 @@ TEST_DEPS = [
"//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-test",
"//third_party:org_mockito_mockito-core",
"//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
"//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module",
"//utility/src/main/java/org/oppia/android/util/logging:prod_module",
"//utility/src/main/java/org/oppia/android/util/networking:debug_module",
diff --git a/testing/build.gradle b/testing/build.gradle
index ef2b75ced3a..f139aa1bbf0 100644
--- a/testing/build.gradle
+++ b/testing/build.gradle
@@ -57,6 +57,7 @@ dependencies {
'androidx.test.espresso:espresso-core:3.2.0',
'androidx.test:runner:1.2.0',
'com.google.dagger:dagger:2.24',
+ 'com.google.protobuf:protobuf-javalite:3.17.3',
'com.google.truth:truth:0.43',
'nl.dionsegijn:konfetti:1.2.5',
'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2',
diff --git a/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel
new file mode 100644
index 00000000000..e57cf3bf175
--- /dev/null
+++ b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel
@@ -0,0 +1,26 @@
+# TODO(#1532): Rename file to 'BUILD' post-Gradle.
+"""
+Package for common test utilities corresponding to data processing & data providers.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
+
+kt_android_library(
+ name = "data_provider_test_monitor",
+ testonly = True,
+ srcs = [
+ "DataProviderTestMonitor.kt",
+ ],
+ visibility = ["//:oppia_testing_visibility"],
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/mockito",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers",
+ "//third_party:org_mockito_mockito-core",
+ "//utility/src/main/java/org/oppia/android/util/data:data_provider",
+ "//utility/src/main/java/org/oppia/android/util/data:data_providers",
+ ],
+)
+
+dagger_rules()
diff --git a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt
new file mode 100644
index 00000000000..441e484d238
--- /dev/null
+++ b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt
@@ -0,0 +1,179 @@
+package org.oppia.android.testing.data
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.oppia.android.testing.data.DataProviderTestMonitor.Factory
+import org.oppia.android.testing.mockito.anyOrNull
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProvider
+import org.oppia.android.util.data.DataProviders.Companion.toLiveData
+import javax.inject.Inject
+
+// TODO(#3813): Migrate all data provider tests over to using this utility.
+/**
+ * A test monitor for [DataProvider]s that provides operations to simplify waiting for the
+ * provider's results, or to verify that notifications actually change the data provider when
+ * expected.
+ *
+ * Note that this utility leverages [TestCoroutineDispatchers] to synchronize data providers with
+ * the test thread. This may result in other operations unintentionally being completed, so care
+ * should be taken when using this monitor in complex multi-operation scenarios. Further, this
+ * utility does not internally support synchronizing against data providers which perform operations
+ * at a future time (i.e. those that would require [TestCoroutineDispatchers.advanceTimeBy] to
+ * synchronize). There are separate methods to use for these cases (e.g.
+ * [ensureNextResultIsSuccess]) which can be called after manually synchronizing test dispatchers
+ * (even for future timed operations).
+ *
+ * To use this monitor, inject its [Factory] and either create a new monitor or use an available
+ * helper method.
+ */
+class DataProviderTestMonitor private constructor(
+ private val testCoroutineDispatchers: TestCoroutineDispatchers,
+ private val dataProvider: DataProvider
+) {
+ private val mockObserver by lazy { createMock>>() }
+ private val resultCaptor by lazy { createCaptor>() }
+ private val liveData: LiveData> by lazy { dataProvider.toLiveData() }
+
+ /**
+ * Waits for the data provider to execute & returns the most recent [AsyncResult] produced by the
+ * provider.
+ *
+ * This method assumes that the data provider has at least one update (which may be any result).
+ *
+ * This can be useful to verify that other operations notify the data provider since subsequent
+ * calls to this method will reset state to ensure the latest state is always being observed.
+ */
+ fun waitForNextResult(): AsyncResult {
+ reset(mockObserver)
+ testCoroutineDispatchers.runCurrent()
+ return ensureNextResultIsPresent()
+ }
+
+ /**
+ * Same as [waitForNextResult] except this also assumes that the most recent result is a success &
+ * then returns the success value of the result.
+ */
+ fun waitForNextSuccessResult(): T = retrieveSuccess(this::waitForNextResult)
+
+ /**
+ * A version of [waitForNextSuccessResult] that doesn't wait for the operation. This is useful for
+ * cases when the calling code is already synchronizing dispatchers and doing so again would break
+ * monitor behavior.
+ */
+ fun ensureNextResultIsSuccess(): T = retrieveSuccess(this::ensureNextResultIsPresent)
+
+ /**
+ * Same as [waitForNextResult] except this also assumes that the most recent result is a failure &
+ * then returns the reason for the result's failure.
+ */
+ fun waitForNextFailingResult(): Throwable = retrieveFailing(this::waitForNextResult)
+
+ /** Same as [ensureNextResultIsSuccess] except for failing cases instead of successes. */
+ fun ensureNextResultIsFailing(): Throwable = retrieveFailing(this::ensureNextResultIsPresent)
+
+ /**
+ * Waits for the data provider to potentially update, then verifies that no new result is
+ * available from the provider. This can be used in contrast with [waitForNextResult] to validate
+ * that the data provider has not been been notified or updated.
+ */
+ fun verifyProviderIsNotUpdated() {
+ reset(mockObserver)
+ testCoroutineDispatchers.runCurrent()
+ verify(mockObserver, never()).onChanged(anyOrNull())
+ }
+
+ private fun startObservingDataProvider() {
+ liveData.observeForever(mockObserver)
+ }
+
+ private fun stopObservingDataProvider() {
+ liveData.removeObserver(mockObserver)
+ }
+
+ private fun ensureNextResultIsPresent(): AsyncResult {
+ // Note to reader: if you encounter the following line in a stack trace then that means the
+ // monitored data provider has not updated in the time before calling one of the waitFor* or
+ // ensure* methods. For uses of ensure* methods, this might mean you should use a waitFor*
+ // method, instead. For uses of waitFor* methods, this might mean you're using one of the
+ // Factory convenience methods instead of creating a monitor. If you're using a waitFor* method
+ // and an explicit monitor, it either means the provider hasn't updated or it requires advancing
+ // the clock (in which case you should use TestCoroutineDispatchers.advanceUntilIdle() and an
+ // ensure* method from this class).
+ verify(mockObserver, atLeastOnce()).onChanged(resultCaptor.capture())
+ reset(mockObserver)
+ return resultCaptor.value
+ }
+
+ private fun retrieveSuccess(operation: () -> AsyncResult): T {
+ return operation().also {
+ // Sanity check.
+ check(it.isSuccess()) { "Expected next result to be a success, not: $it" }
+ }.getOrThrow()
+ }
+
+ private fun retrieveFailing(operation: () -> AsyncResult): Throwable {
+ return operation().also {
+ // Sanity check.
+ check(it.isFailure()) { "Expected next result to be a failure, not: $it" }
+ }.getErrorOrNull() ?: error("Expect result to have a failure error")
+ }
+
+ /**
+ * Factory for creating new [DataProviderTestMonitor]s. This class should be injected within tests
+ * at the application scope & requires the host test application component to include the
+ * necessary modules for [TestCoroutineDispatchers].
+ */
+ class Factory @Inject constructor(
+ private val testCoroutineDispatchers: TestCoroutineDispatchers
+ ) {
+ /** Returns a new monitor for the specified [DataProvider]. */
+ fun createMonitor(dataProvider: DataProvider): DataProviderTestMonitor {
+ return DataProviderTestMonitor(testCoroutineDispatchers, dataProvider).also {
+ // Immediately start observing since it doesn't make sense not to always be observing for
+ // the current monitor.
+ it.startObservingDataProvider()
+ }
+ }
+
+ /**
+ * Convenience function for monitoring the specified data provider & waiting for its next result
+ * (expected to be a success). See [waitForNextSuccessResult] for specifics.
+ *
+ * This method ensures that only one result is ever captured from the data provider.
+ */
+ fun waitForNextSuccessfulResult(dataProvider: DataProvider): T {
+ val monitor = createMonitor(dataProvider)
+ return monitor.waitForNextSuccessResult().also {
+ monitor.stopObservingDataProvider()
+ }
+ }
+
+ /**
+ * Convenience function for monitoring the specified data provider & waiting for its next result
+ * (expected to be a failure). See [waitForNextFailingResult] for specifics.
+ *
+ * This method ensures that only one result is ever captured from the data provider.
+ */
+ fun waitForNextFailureResult(dataProvider: DataProvider): Throwable {
+ val monitor = createMonitor(dataProvider)
+ return monitor.waitForNextFailingResult().also {
+ monitor.stopObservingDataProvider()
+ }
+ }
+ }
+
+ private companion object {
+ private inline fun createMock(): T = mock(T::class.java)
+
+ private inline fun createCaptor(): ArgumentCaptor =
+ ArgumentCaptor.forClass(T::class.java)
+ }
+}
diff --git a/testing/src/main/java/org/oppia/android/testing/robolectric/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/robolectric/BUILD.bazel
index c6ee41183b7..af491298ca6 100644
--- a/testing/src/main/java/org/oppia/android/testing/robolectric/BUILD.bazel
+++ b/testing/src/main/java/org/oppia/android/testing/robolectric/BUILD.bazel
@@ -31,4 +31,22 @@ kt_android_library(
],
)
+# Note that this library is visibility restricted to avoid wide use of custom shadows in the
+# codebase.
+kt_android_library(
+ name = "shadow_bidi_formatter",
+ testonly = True,
+ srcs = [
+ "ShadowBidiFormatter.kt",
+ ],
+ visibility = [
+ "//testing/src/test/java/org/oppia/android/testing/robolectric:__pkg__",
+ "//utility/src/test/java/org/oppia/android/util/locale:__pkg__",
+ "//utility/src/test/java/org/oppia/android/util/locale/testing:__pkg__",
+ ],
+ deps = [
+ "//third_party:org_robolectric_robolectric",
+ ],
+)
+
dagger_rules()
diff --git a/testing/src/main/java/org/oppia/android/testing/robolectric/ShadowBidiFormatter.kt b/testing/src/main/java/org/oppia/android/testing/robolectric/ShadowBidiFormatter.kt
new file mode 100644
index 00000000000..a24d0daaed3
--- /dev/null
+++ b/testing/src/main/java/org/oppia/android/testing/robolectric/ShadowBidiFormatter.kt
@@ -0,0 +1,105 @@
+package org.oppia.android.testing.robolectric
+
+import android.text.BidiFormatter
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+import org.robolectric.annotation.RealObject
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.util.ReflectionHelpers
+import java.util.Locale
+
+/**
+ * A custom Robolectric shadow for tracking interactions with [BidiFormatter].
+ *
+ * This is the main way to ensure that the wrapper actually calls through to Android's
+ * bidirectional formatter. Note that the reason this isn't used broadly is for better
+ * compatibility:
+ * - Shadows are complicated to set up (versus using existing Dagger module mechanism with
+ * ubiquitous examples)
+ * - Shadows don't work when running shared app layer tests with Espresso
+ * - Custom shadows are discouraged, so this restricts use
+ * - The wrapper can be replaced with a test double which can more easily be configured to rely on
+ * the real implementation & perform test-only tracking that's a bit more complicated with the
+ * shadow
+ * - Shadow static state can leak between tests whereas singleton state in Dagger is properly
+ * recreated between test application instances
+ */
+@Implements(BidiFormatter::class)
+class ShadowBidiFormatter {
+ companion object {
+ private val trackedFormatters = mutableMapOf()
+
+ /**
+ * Returns new instances of [BidiFormatter] based on the specified [Locale]. This is called by
+ * Robolectric as a replacement to [BidiFormatter.getInstance].
+ *
+ * Note that Android may reuse formatters internally, so there's no guarantee a new
+ * implementation will be returned for subsequent calls (even if different [Locale]s are used).
+ */
+ @Implementation
+ @JvmStatic
+ fun getInstance(locale: Locale): BidiFormatter {
+ return trackedFormatters.getOrPut(locale) {
+ Shadow.directlyOn(
+ BidiFormatter::class.java, "getInstance",
+ ReflectionHelpers.ClassParameter.from(Locale::class.java, locale)
+ )
+ }
+ }
+
+ /**
+ * Returns the [ShadowBidiFormatter] corresponding to the specified [Locale] (as created with
+ * [getInstance]) or null if none exists.
+ */
+ fun lookUpFormatter(locale: Locale): ShadowBidiFormatter? =
+ trackedFormatters[locale]?.let { shadowOf(it) }
+
+ /**
+ * Returns all [ShadowBidiFormatter]s created via [getInstance] since the last call to [reset].
+ */
+ fun lookUpFormatters(): Map =
+ trackedFormatters.mapValues { (_, formatter) -> shadowOf(formatter) }
+
+ /**
+ * Resets all tracked formatters up to now. This should always be called in a tear-down method
+ * to avoid leaking state between tests.
+ */
+ fun reset() {
+ // The tracked formatters are cleared to make it seem like new formatters are being created.
+ // Similarly each individual shadow needs to be reset since Android only creates a couple of
+ // Bidi formatters & Robolectric will keep a 1:1 relationship between classes and their
+ // shadows.
+ lookUpFormatters().values.forEach { it.wrappedSequences.clear() }
+ trackedFormatters.clear()
+ }
+
+ private fun shadowOf(bidiFormatter: BidiFormatter): ShadowBidiFormatter =
+ Shadow.extract(bidiFormatter) as ShadowBidiFormatter
+ }
+
+ @RealObject
+ private lateinit var realObject: BidiFormatter
+
+ private val wrappedSequences = mutableListOf()
+
+ /**
+ * Robolectric shadow override of [BidiFormatter.unicodeWrap]. Note that only the [CharSequence]
+ * version of unicode wrap is implemented here, so callers should make sure to only use that
+ * overload.
+ */
+ @Suppress("unused") // Incorrect warning; Robolectric uses this via reflection.
+ @Implementation
+ fun unicodeWrap(str: CharSequence): CharSequence {
+ wrappedSequences += str
+ return Shadow.directlyOn(
+ realObject, BidiFormatter::class.java, "unicodeWrap",
+ ReflectionHelpers.ClassParameter.from(CharSequence::class.java, str)
+ )
+ }
+
+ /** Returns the most recent wrapped sequence as per the call to [unicodeWrap]. */
+ fun getLastWrappedSequence(): CharSequence? = wrappedSequences.lastOrNull()
+
+ /** Returns all sequences passed to [unicodeWrap] for this formatter. */
+ fun getAllWrappedSequences(): List = wrappedSequences
+}
diff --git a/testing/src/main/java/org/oppia/android/testing/time/FakeOppiaClock.kt b/testing/src/main/java/org/oppia/android/testing/time/FakeOppiaClock.kt
index 3bcb944c283..d2c3b433902 100644
--- a/testing/src/main/java/org/oppia/android/testing/time/FakeOppiaClock.kt
+++ b/testing/src/main/java/org/oppia/android/testing/time/FakeOppiaClock.kt
@@ -2,6 +2,7 @@ package org.oppia.android.testing.time
import android.annotation.SuppressLint
import android.os.SystemClock
+import org.oppia.android.testing.time.FakeOppiaClock.FakeTimeMode
import org.oppia.android.util.system.OppiaClock
import java.text.SimpleDateFormat
import java.util.Calendar
@@ -24,6 +25,11 @@ class FakeOppiaClock @Inject constructor() : OppiaClock {
private var fixedFakeTimeMs: Long = 0
private var fakeTimeMode: FakeTimeMode = FakeTimeMode.MODE_WALL_CLOCK_TIME
+ init {
+ // Ensure tests that rely on this clock are always operating in UTC.
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
+ }
+
override fun getCurrentTimeMs(): Long {
return when (fakeTimeMode) {
FakeTimeMode.MODE_WALL_CLOCK_TIME -> System.currentTimeMillis()
@@ -32,12 +38,6 @@ class FakeOppiaClock @Inject constructor() : OppiaClock {
}
}
- override fun getCurrentCalendar(): Calendar {
- val calendar = Calendar.getInstance()
- calendar.timeInMillis = getCurrentTimeMs()
- return calendar
- }
-
/**
* Sets the current wall-clock time in milliseconds since the Unix epoch, in UTC.
*
diff --git a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel
new file mode 100644
index 00000000000..4b84736203b
--- /dev/null
+++ b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel
@@ -0,0 +1,37 @@
+# TODO(#1532): Rename file to 'BUILD' post-Gradle.
+"""
+Tests for data processing test utilities.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "DataProviderTestMonitorTest",
+ srcs = ["DataProviderTestMonitorTest.kt"],
+ custom_package = "org.oppia.android.testing.data",
+ test_class = "org.oppia.android.testing.data.DataProviderTestMonitorTest",
+ test_manifest = "//testing:test_manifest",
+ deps = [
+ ":dagger",
+ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller",
+ "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller",
+ "//model:languages_java_proto_lite",
+ "//testing",
+ "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/data:data_providers",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ "//utility/src/main/java/org/oppia/android/util/networking:debug_module",
+ ],
+)
+
+dagger_rules()
diff --git a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt
new file mode 100644
index 00000000000..152ea5a7dc9
--- /dev/null
+++ b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt
@@ -0,0 +1,888 @@
+package org.oppia.android.testing.data
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.exceptions.verification.NeverWantedButInvoked
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.assertThrows
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.data.AsyncDataSubscriptionManager
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProvider
+import org.oppia.android.util.data.DataProviders
+import org.oppia.android.util.data.DataProvidersInjector
+import org.oppia.android.util.data.DataProvidersInjectorProvider
+import org.oppia.android.util.locale.LocaleProdModule
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [DataProviderTestMonitor]. */
+// FunctionName: test names are conventionally named with underscores.
+// SameParameterValue: tests should have specific context included/excluded for readability.
+// ThrowableNotThrown: exceptions are created for AsyncResults.
+@Suppress("FunctionName", "SameParameterValue", "ThrowableNotThrown")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = DataProviderTestMonitorTest.TestApplication::class)
+class DataProviderTestMonitorTest {
+ @Inject
+ lateinit var monitorFactory: DataProviderTestMonitor.Factory
+
+ @Inject
+ lateinit var dataProviders: DataProviders
+
+ @Inject
+ lateinit var asyncDataSubscriptionManager: AsyncDataSubscriptionManager
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ /* Tests for createMonitor */
+
+ @Test
+ fun testCreateMonitor_returnsNewObservingMonitor() {
+ var fakeLoadMemoryCallbackCalled = false
+ val fakeLoadMemoryCallback: () -> String = {
+ fakeLoadMemoryCallbackCalled = true
+ "test value"
+ }
+ val dataProvider = dataProviders.createInMemoryDataProvider("test", fakeLoadMemoryCallback)
+
+ monitorFactory.createMonitor(dataProvider)
+ testCoroutineDispatchers.runCurrent()
+
+ // Verify that the data provider was executed (indicating that the monitor is live).
+ assertThat(fakeLoadMemoryCallbackCalled).isTrue()
+ }
+
+ @Test
+ fun testCreateMonitor_twice_returnsDifferentMonitors() {
+ val dataProvider = dataProviders.createInMemoryDataProvider("test") { 0 }
+
+ val monitor1 = monitorFactory.createMonitor(dataProvider)
+ val monitor2 = monitorFactory.createMonitor(dataProvider)
+
+ // Verify that the two monitors are different
+ assertThat(monitor1).isNotEqualTo(monitor2)
+ }
+
+ /* Tests for waitForNextResult */
+
+ @Test
+ fun testWaitForNextResult_pendingDataProvider_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val result = monitor.waitForNextResult()
+
+ assertThat(result.isPending()).isTrue()
+ }
+
+ @Test
+ fun testWaitForNextResult_failingDataProvider_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val result = monitor.waitForNextResult()
+
+ assertThat(result.isFailure()).isTrue()
+ assertThat(result.getErrorOrNull()).hasMessageThat().contains("Failure")
+ }
+
+ @Test
+ fun testWaitForNextResult_successfulDataProvider_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val result = monitor.waitForNextResult()
+
+ assertThat(result.isSuccess()).isTrue()
+ assertThat(result.getOrThrow()).isEqualTo("str value")
+ }
+
+ @Test
+ fun testWaitForNextResult_failureThenSuccess_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ // Update the provider, then wait for the result.
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+ val result = monitor.waitForNextResult()
+
+ assertThat(result.isSuccess()).isTrue()
+ assertThat(result.getOrThrow()).isEqualTo("str value")
+ }
+
+ @Test
+ fun testWaitForNextResult_differentValues_notified_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("first"), AsyncResult.success("second")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ monitor.waitForNextResult()
+
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+ val result = monitor.waitForNextResult()
+
+ assertThat(result.isSuccess()).isTrue()
+ assertThat(result.getOrThrow()).isEqualTo("second")
+ }
+
+ /* Tests for waitForNextSuccessResult */
+
+ @Test
+ fun testWaitForNextSuccessResult_pendingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val failure = assertThrows(IllegalStateException::class) { monitor.waitForNextSuccessResult() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testWaitForNextSuccessResult_failingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val failure = assertThrows(IllegalStateException::class) { monitor.waitForNextSuccessResult() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testWaitForNextSuccessResult_successfulDataProvider_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val result = monitor.waitForNextSuccessResult()
+
+ assertThat(result).isEqualTo("str value")
+ }
+
+ @Test
+ fun testWaitForNextSuccessResult_failureThenSuccess_notified_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ val result = monitor.waitForNextSuccessResult()
+
+ assertThat(result).isEqualTo("str value")
+ }
+
+ @Test
+ fun testWaitForNextSuccessResult_successThenFailure_notified_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ val failure = assertThrows(IllegalStateException::class) { monitor.waitForNextSuccessResult() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testWaitForNextSuccessResult_differentValues_notified_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("first"), AsyncResult.success("second")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ val result = monitor.waitForNextSuccessResult()
+
+ assertThat(result).isEqualTo("second")
+ }
+
+ /* Tests for ensureNextResultIsSuccess */
+
+ @Test
+ fun testEnsureNextResultIsSuccess_successfulDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ // Internal expectation failure since the operation hasn't completed.
+ assertThrows(AssertionError::class) { monitor.ensureNextResultIsSuccess() }
+ }
+
+ @Test
+ fun testEnsureNextResultIsSuccess_successfulDataProvider_wait_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ testCoroutineDispatchers.runCurrent()
+ val result = monitor.ensureNextResultIsSuccess()
+
+ assertThat(result).isEqualTo("str value")
+ }
+
+ @Test
+ fun testEnsureNextResultIsSuccess_pendingDataProvider_wait_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ testCoroutineDispatchers.runCurrent()
+ val failure = assertThrows(IllegalStateException::class) { monitor.ensureNextResultIsSuccess() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testEnsureNextResultIsSuccess_failingDataProvider_wait_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ testCoroutineDispatchers.runCurrent()
+ val failure = assertThrows(IllegalStateException::class) { monitor.ensureNextResultIsSuccess() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testEnsureNextResultIsSuccess_failureThenSuccess_notified_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ // Internal expectation failure since the operation hasn't completed.
+ assertThrows(AssertionError::class) { monitor.ensureNextResultIsSuccess() }
+ }
+
+ @Test
+ fun testEnsureNextResultIsSuccess_failureThenSuccess_notified_wait_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ testCoroutineDispatchers.runCurrent()
+ val result = monitor.ensureNextResultIsSuccess()
+
+ assertThat(result).isEqualTo("str value")
+ }
+
+ @Test
+ fun testEnsureNextResultIsSuccess_successThenFailure_notified_wait_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ testCoroutineDispatchers.runCurrent()
+ val failure = assertThrows(IllegalStateException::class) { monitor.ensureNextResultIsSuccess() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testEnsureNextResultIsSuccess_differentValues_notified_wait_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("first"), AsyncResult.success("second")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ testCoroutineDispatchers.runCurrent()
+ val result = monitor.ensureNextResultIsSuccess()
+
+ assertThat(result).isEqualTo("second")
+ }
+
+ /* Tests for waitForNextFailingResult */
+
+ @Test
+ fun testWaitForNextFailingResult_pendingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val failure = assertThrows(IllegalStateException::class) { monitor.waitForNextFailingResult() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testWaitForNextFailingResult_failingDataProvider_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val result = monitor.waitForNextFailingResult()
+
+ assertThat(result).hasMessageThat().contains("Failure")
+ }
+
+ @Test
+ fun testWaitForNextFailingResult_successfulDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ val failure = assertThrows(IllegalStateException::class) { monitor.waitForNextFailingResult() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testWaitForNextFailingResult_successThenFailure_notified_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ val result = monitor.waitForNextFailingResult()
+
+ assertThat(result).hasMessageThat().contains("Failure")
+ }
+
+ @Test
+ fun testWaitForNextFailingResult_failureThenSuccess_notified_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ val failure = assertThrows(IllegalStateException::class) { monitor.waitForNextFailingResult() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testWaitForNextFailingResult_differentValues_notified_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ val result = monitor.waitForNextFailingResult()
+
+ assertThat(result).hasMessageThat().contains("Second")
+ }
+
+ /* Tests for ensureNextResultIsFailing */
+
+ @Test
+ fun testEnsureNextResultIsFailing_failingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ // Internal expectation failure since the operation hasn't completed.
+ assertThrows(AssertionError::class) { monitor.ensureNextResultIsSuccess() }
+ }
+
+ @Test
+ fun testEnsureNextResultIsFailing_failingDataProvider_wait_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ testCoroutineDispatchers.runCurrent()
+ val result = monitor.ensureNextResultIsFailing()
+
+ assertThat(result).hasMessageThat().contains("Failure")
+ }
+
+ @Test
+ fun testEnsureNextResultIsFailing_pendingDataProvider_wait_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ testCoroutineDispatchers.runCurrent()
+ val failure = assertThrows(IllegalStateException::class) { monitor.ensureNextResultIsFailing() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testEnsureNextResultIsFailing_successfulDataProvider_wait_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ testCoroutineDispatchers.runCurrent()
+ val failure = assertThrows(IllegalStateException::class) { monitor.ensureNextResultIsFailing() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testEnsureNextResultIsFailing_successThenFailure_notified_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ // Internal expectation failure since the operation hasn't completed.
+ assertThrows(AssertionError::class) { monitor.ensureNextResultIsSuccess() }
+ }
+
+ @Test
+ fun testEnsureNextResultIsFailing_successThenFailure_notified_wait_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ testCoroutineDispatchers.runCurrent()
+ val result = monitor.ensureNextResultIsFailing()
+
+ assertThat(result).hasMessageThat().contains("Failure")
+ }
+
+ @Test
+ fun testEnsureNextResultIsFailing_failureThenSuccess_notified_wait_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ testCoroutineDispatchers.runCurrent()
+ val failure = assertThrows(IllegalStateException::class) { monitor.ensureNextResultIsFailing() }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testEnsureNextResultIsFailing_differentValues_notified_wait_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ testCoroutineDispatchers.runCurrent()
+ val result = monitor.ensureNextResultIsFailing()
+
+ assertThat(result).hasMessageThat().contains("Second")
+ }
+
+ /* Tests for verifyProviderIsNotUpdated */
+
+ @Test
+ fun testVerifyProviderIsNotUpdated_pendingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ // Verify that the method wsa actually called despite not being expected to have been.
+ assertThrows(NeverWantedButInvoked::class) { monitor.verifyProviderIsNotUpdated() }
+ }
+
+ @Test
+ fun testVerifyProviderIsNotUpdated_failingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ // Verify that the method wsa actually called despite not being expected to have been.
+ assertThrows(NeverWantedButInvoked::class) { monitor.verifyProviderIsNotUpdated() }
+ }
+
+ @Test
+ fun testVerifyProviderIsNotUpdated_successfulDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ // Verify that the method wsa actually called despite not being expected to have been.
+ assertThrows(NeverWantedButInvoked::class) { monitor.verifyProviderIsNotUpdated() }
+ }
+
+ @Test
+ fun testVerifyProviderIsNotUpdated_successThenFailure_notified_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ // Verify that the method wsa actually called despite not being expected to have been.
+ assertThrows(NeverWantedButInvoked::class) { monitor.verifyProviderIsNotUpdated() }
+ }
+
+ @Test
+ fun testVerifyProviderIsNotUpdated_failureThenSuccess_notified_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+
+ // Update the provider, then wait for the result.
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ // Verify that the method wsa actually called despite not being expected to have been.
+ assertThrows(NeverWantedButInvoked::class) { monitor.verifyProviderIsNotUpdated() }
+ }
+
+ @Test
+ fun testVerifyProviderIsNotUpdated_differentValues_notified_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("first"), AsyncResult.success("second")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ monitor.waitForNextResult()
+
+ asyncDataSubscriptionManager.notifyChangeAsync("test")
+
+ // Verify that the method wsa actually called despite not being expected to have been.
+ assertThrows(NeverWantedButInvoked::class) { monitor.verifyProviderIsNotUpdated() }
+ }
+
+ @Test
+ fun testVerifyProviderIsNotUpdated_waitForSuccess_noChanges_doesNotThrowException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("first"), AsyncResult.success("second")
+ )
+ val monitor = monitorFactory.createMonitor(dataProvider)
+ monitor.waitForNextSuccessResult()
+
+ monitor.verifyProviderIsNotUpdated()
+
+ // The verification check doesn't throw since nothing's changed since the first result was
+ // retrieved.
+ }
+
+ /* Tests for waitForNextSuccessfulResult */
+
+ @Test
+ fun testWaitForNextSuccessfulResult_pendingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+
+ val failure =
+ assertThrows(IllegalStateException::class) {
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+ }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testWaitForNextSuccessfulResult_failingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+
+ val failure = assertThrows(IllegalStateException::class) {
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+ }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testWaitForNextSuccessfulResult_successfulDataProvider_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+
+ val result = monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ assertThat(result).isEqualTo("str value")
+ }
+
+ @Test
+ fun testWaitForNextSuccessfulResult_failureThenSuccess_consumed_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ monitorFactory.waitForNextFailureResult(dataProvider)
+
+ val result = monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ assertThat(result).isEqualTo("str value")
+ }
+
+ @Test
+ fun testWaitForNextSuccessfulResult_successThenFailure_consumed_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val failure = assertThrows(IllegalStateException::class) {
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+ }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a success")
+ }
+
+ @Test
+ fun testWaitForNextSuccessfulResult_differentValues_consumed_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("first"), AsyncResult.success("second")
+ )
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val result = monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ assertThat(result).isEqualTo("second")
+ }
+
+ @Test
+ fun testWaitForNextSuccessfulResult_twiceForChangedProvider_returnsCorrectValues() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("first"), AsyncResult.success("second")
+ )
+
+ val firstResult = monitorFactory.waitForNextSuccessfulResult(dataProvider)
+ val secondResult = monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ assertThat(firstResult).isEqualTo("first")
+ assertThat(secondResult).isEqualTo("second")
+ }
+
+ /* Tests for waitForNextFailureResult */
+
+ @Test
+ fun testWaitForNextFailureResult_pendingDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.pending()
+ }
+
+ val failure = assertThrows(IllegalStateException::class) {
+ monitorFactory.waitForNextFailureResult(dataProvider)
+ }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testWaitForNextFailureResult_failingDataProvider_returnsResult() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.failed(Exception("Failure"))
+ }
+
+ val result = monitorFactory.waitForNextFailureResult(dataProvider)
+
+ assertThat(result).hasMessageThat().contains("Failure")
+ }
+
+ @Test
+ fun testWaitForNextFailureResult_successfulDataProvider_throwsException() {
+ val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") {
+ AsyncResult.success("str value")
+ }
+
+ val failure = assertThrows(IllegalStateException::class) {
+ monitorFactory.waitForNextFailureResult(dataProvider)
+ }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testWaitForNextFailureResult_successThenFailure_consumed_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure"))
+ )
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val result = monitorFactory.waitForNextFailureResult(dataProvider)
+
+ assertThat(result).hasMessageThat().contains("Failure")
+ }
+
+ @Test
+ fun testWaitForNextFailureResult_failureThenSuccess_consumed_throwsException() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value")
+ )
+ monitorFactory.waitForNextFailureResult(dataProvider)
+
+ val failure = assertThrows(IllegalStateException::class) {
+ monitorFactory.waitForNextFailureResult(dataProvider)
+ }
+
+ assertThat(failure).hasMessageThat().contains("Expected next result to be a failure")
+ }
+
+ @Test
+ fun testWaitForNextFailureResult_differentValues_consumed_returnsLatest() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second"))
+ )
+ monitorFactory.waitForNextFailureResult(dataProvider)
+
+ val result = monitorFactory.waitForNextFailureResult(dataProvider)
+
+ assertThat(result).hasMessageThat().contains("Second")
+ }
+
+ @Test
+ fun testWaitForNextFailureResult_twiceForChangedProvider_returnsCorrectValues() {
+ val dataProvider =
+ createDataProviderWithResultsQueue(
+ "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second"))
+ )
+
+ val firstResult = monitorFactory.waitForNextFailureResult(dataProvider)
+ val secondResult = monitorFactory.waitForNextFailureResult(dataProvider)
+
+ assertThat(firstResult).hasMessageThat().contains("First")
+ assertThat(secondResult).hasMessageThat().contains("Second")
+ }
+
+ private fun createDataProviderWithResultsQueue(
+ id: Any,
+ vararg results: AsyncResult
+ ): DataProvider {
+ val resultsQueue = createResultQueue(*results)
+ return dataProviders.createInMemoryDataProviderAsync(id) {
+ resultsQueue.removeFirst()
+ }
+ }
+
+ private fun createResultQueue(vararg results: AsyncResult): ArrayDeque> {
+ return ArrayDeque(results.toList())
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LogStorageModule::class, NetworkConnectionUtilDebugModule::class,
+ TestLogReportingModule::class, LoggerModule::class, TestDispatcherModule::class,
+ LocaleProdModule::class, FakeOppiaClockModule::class, RobolectricModule::class
+ ]
+ )
+ interface TestApplicationComponent : DataProvidersInjector {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(dataProviderTestMonitorTest: DataProviderTestMonitorTest)
+ }
+
+ class TestApplication : Application(), DataProvidersInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerDataProviderTestMonitorTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build()
+ }
+
+ fun inject(dataProviderTestMonitorTest: DataProviderTestMonitorTest) {
+ component.inject(dataProviderTestMonitorTest)
+ }
+
+ override fun getDataProvidersInjector(): DataProvidersInjector = component
+ }
+}
diff --git a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt
index f7c2628ab61..23f5b99dd68 100644
--- a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt
+++ b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt
@@ -37,6 +37,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.CacheAssetsLocally
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.caching.TopicListToCache
@@ -290,7 +291,7 @@ class ExplorationCheckpointTestHelperTest {
modules = [
TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
TestDispatcherModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
- ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class
+ ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class, AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/testing/src/test/java/org/oppia/android/testing/robolectric/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/robolectric/BUILD.bazel
new file mode 100644
index 00000000000..8e92cbfb7b0
--- /dev/null
+++ b/testing/src/test/java/org/oppia/android/testing/robolectric/BUILD.bazel
@@ -0,0 +1,25 @@
+"""
+Tests for Robolectric-specific utilities and configurations.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "ShadowBidiFormatterTest",
+ srcs = ["ShadowBidiFormatterTest.kt"],
+ custom_package = "org.oppia.android.testing.robolectric",
+ test_class = "org.oppia.android.testing.robolectric.ShadowBidiFormatterTest",
+ test_manifest = "//testing:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:shadow_bidi_formatter",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ ],
+)
+
+dagger_rules()
diff --git a/testing/src/test/java/org/oppia/android/testing/robolectric/ShadowBidiFormatterTest.kt b/testing/src/test/java/org/oppia/android/testing/robolectric/ShadowBidiFormatterTest.kt
new file mode 100644
index 00000000000..11838bd2300
--- /dev/null
+++ b/testing/src/test/java/org/oppia/android/testing/robolectric/ShadowBidiFormatterTest.kt
@@ -0,0 +1,154 @@
+package org.oppia.android.testing.robolectric
+
+import android.app.Application
+import android.content.Context
+import android.text.BidiFormatter
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import java.util.Locale
+import javax.inject.Singleton
+
+/** Tests for [ShadowBidiFormatter]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE, shadows = [ShadowBidiFormatter::class])
+class ShadowBidiFormatterTest {
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @After
+ fun tearDown() {
+ // Make sure this is reset between tests.
+ ShadowBidiFormatter.reset()
+ }
+
+ @Test
+ fun testCustomShadow_initialState_noFormattersCreator_hasZeroTrackedFormatters() {
+ assertThat(ShadowBidiFormatter.lookUpFormatters()).isEmpty()
+ assertThat(ShadowBidiFormatter.lookUpFormatter(Locale.US)).isNull()
+ }
+
+ @Test
+ fun testCustomShadow_getFormatterInstance_addsTrackedFormatter() {
+ BidiFormatter.getInstance(Locale.US)
+
+ assertThat(ShadowBidiFormatter.lookUpFormatters()).hasSize(1)
+ assertThat(ShadowBidiFormatter.lookUpFormatters()).containsKey(Locale.US)
+ assertThat(ShadowBidiFormatter.lookUpFormatter(Locale.US)).isNotNull()
+ }
+
+ @Test
+ fun testCustomShadow_createFormatter_hasNoTrackedStrings() {
+ BidiFormatter.getInstance(Locale.US)
+
+ val shadow = ShadowBidiFormatter.lookUpFormatter(Locale.US)
+ assertThat(shadow?.getLastWrappedSequence()).isNull()
+ assertThat(shadow?.getAllWrappedSequences()).isEmpty()
+ }
+
+ @Test
+ fun testCustomShadow_createFormatter_wrapNonCharSequence_doesNotContainString() {
+ val formatter = BidiFormatter.getInstance(Locale.US)
+
+ formatter.unicodeWrap("test string")
+
+ // This slightly weaker check is needed since the internal implementation of BidiFormatter seems
+ // like it might call the CharSequence version of unicodeWrap(), so no assumptions can be made
+ // about the exact calls.
+ val shadow = ShadowBidiFormatter.lookUpFormatter(Locale.US)
+ assertThat(shadow?.getAllWrappedSequences()).doesNotContain("test string")
+ }
+
+ @Test
+ fun testCustomShadow_createFormatter_wrapString_tracksWrappedString() {
+ val formatter = BidiFormatter.getInstance(Locale.US)
+
+ formatter.unicodeWrap("test string" as CharSequence)
+
+ val shadow = ShadowBidiFormatter.lookUpFormatter(Locale.US)
+ assertThat(shadow?.getLastWrappedSequence()).isEqualTo("test string")
+ }
+
+ @Test
+ fun testCustomShadow_createFormatter_wrapStrings_tracksLatestString() {
+ val formatter = BidiFormatter.getInstance(Locale.US)
+
+ formatter.unicodeWrap("test string one" as CharSequence)
+ formatter.unicodeWrap("test string two" as CharSequence)
+
+ val shadow = ShadowBidiFormatter.lookUpFormatter(Locale.US)
+ assertThat(shadow?.getLastWrappedSequence()).isEqualTo("test string two")
+ }
+
+ @Test
+ fun testCustomShadow_createFormatter_wrapStrings_tracksAllStringsInOrder() {
+ val formatter = BidiFormatter.getInstance(Locale.US)
+
+ formatter.unicodeWrap("test string one" as CharSequence)
+ formatter.unicodeWrap("test string two" as CharSequence)
+
+ val shadow = ShadowBidiFormatter.lookUpFormatter(Locale.US)
+ assertThat(shadow?.getAllWrappedSequences())
+ .containsAllOf("test string one", "test string two").inOrder()
+ }
+
+ @Test
+ fun testCustomShadow_reset_clearsFormatters() {
+ BidiFormatter.getInstance(Locale.US)
+
+ ShadowBidiFormatter.reset()
+
+ assertThat(ShadowBidiFormatter.lookUpFormatters()).isEmpty()
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerShadowBidiFormatterTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(shadowBidiFormatterTest: ShadowBidiFormatterTest)
+ }
+}
diff --git a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt
index 3eed6951c42..cc027eeba1a 100644
--- a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt
+++ b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt
@@ -59,6 +59,7 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClock
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.data.DataProvider
@@ -1769,7 +1770,8 @@ class StoryProgressTestHelperTest {
modules = [
TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
TestDispatcherModule::class, RobolectricModule::class, FakeOppiaClockModule::class,
- ImageParsingModule::class, LoggerModule::class, NetworkConnectionUtilDebugModule::class
+ ImageParsingModule::class, LoggerModule::class, NetworkConnectionUtilDebugModule::class,
+ AssetModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
diff --git a/testing/src/test/java/org/oppia/android/testing/time/FakeOppiaClockTest.kt b/testing/src/test/java/org/oppia/android/testing/time/FakeOppiaClockTest.kt
index f9afaeb4d31..5b534916126 100644
--- a/testing/src/test/java/org/oppia/android/testing/time/FakeOppiaClockTest.kt
+++ b/testing/src/test/java/org/oppia/android/testing/time/FakeOppiaClockTest.kt
@@ -32,7 +32,9 @@ import org.oppia.android.util.logging.LoggerModule
import org.oppia.android.util.parser.image.ImageParsingModule
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import java.time.ZoneId
import java.util.Calendar
+import java.util.TimeZone
import javax.inject.Inject
import javax.inject.Singleton
@@ -58,6 +60,11 @@ class FakeOppiaClockTest {
setUpTestApplicationComponent()
}
+ @Test
+ fun testInitialState_forcesTimeZoneToUtc() {
+ assertThat(TimeZone.getDefault().toZoneId()).isEqualTo(ZoneId.of("UTC"))
+ }
+
@Test
fun testGetFakeTimeMode_initialState_isWallClockTime() {
val timeMode = fakeOppiaClock.getFakeTimeMode()
diff --git a/third_party/versions.bzl b/third_party/versions.bzl
index d27ab652108..0981f7177bd 100644
--- a/third_party/versions.bzl
+++ b/third_party/versions.bzl
@@ -132,7 +132,7 @@ HTTP_DEPENDENCY_VERSIONS = {
"version": "v1.5.0-alpha-2",
},
"rules_proto": {
- "sha": "602e7161d9195e50246177e7c55b2f39950a9cf7366f74ed5f22fd45750cd208",
+ "sha": "e0cab008a9cdc2400a1d6572167bf9c5afc72e19ee2b862d18581051efab42c9",
"version": "c0b62f2f46c85c16cb3b5e9e921f0d00e3101934",
},
}
diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel
index 68333f33a93..6b8f9f87786 100644
--- a/utility/BUILD.bazel
+++ b/utility/BUILD.bazel
@@ -33,6 +33,14 @@ android_library(
resource_files = glob(["src/main/res/**/*.xml"]),
)
+# Visibility for migrated utility tests.
+package_group(
+ name = "utility_testing_visibility",
+ packages = [
+ "//utility/src/test/...",
+ ],
+)
+
# Library for general-purpose utilities.
kt_android_library(
name = "utility",
@@ -57,7 +65,8 @@ kt_android_library(
"//third_party:com_google_guava_guava",
"//third_party:glide_compiler",
"//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
- "//utility/src/main/java/org/oppia/android/util/caching:assets",
+ "//utility/src/main/java/org/oppia/android/util/caching:annotations",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
"//utility/src/main/java/org/oppia/android/util/gcsresource:annotations",
"//utility/src/main/java/org/oppia/android/util/gcsresource:prod_module",
"//utility/src/main/java/org/oppia/android/util/logging:console_logger",
@@ -95,6 +104,7 @@ TEST_DEPS = [
"//third_party:org_jetbrains_kotlin_kotlin-test-junit",
"//third_party:org_mockito_mockito-core",
"//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
"//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module",
"//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions",
"//utility/src/main/java/org/oppia/android/util/logging:event_bundle_creator",
diff --git a/utility/build.gradle b/utility/build.gradle
index 57a62088322..98fd0e5a4b3 100644
--- a/utility/build.gradle
+++ b/utility/build.gradle
@@ -70,6 +70,7 @@ dependencies {
'com.google.firebase:firebase-analytics-ktx:17.5.0',
'com.google.firebase:firebase-core:17.5.0',
'com.google.firebase:firebase-crashlytics:17.0.0',
+ 'com.google.protobuf:protobuf-javalite:3.17.3',
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version",
)
compileOnly(
@@ -81,6 +82,7 @@ dependencies {
'androidx.test.ext:junit:1.1.1',
'com.google.dagger:dagger:2.24',
'com.google.truth:truth:0.43',
+ 'com.google.truth.extensions:truth-liteproto-extension:0.43',
'junit:junit:4.12',
"org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version",
'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2',
diff --git a/utility/src/main/java/org/oppia/android/util/caching/AssetModule.kt b/utility/src/main/java/org/oppia/android/util/caching/AssetModule.kt
new file mode 100644
index 00000000000..5ad45b26ffb
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/caching/AssetModule.kt
@@ -0,0 +1,11 @@
+package org.oppia.android.util.caching
+
+import dagger.Module
+import dagger.Provides
+
+/** Provides dependencies corresponding to loading assets. */
+@Module
+class AssetModule {
+ @Provides
+ fun provideAssetRepository(impl: AssetRepositoryImpl): AssetRepository = impl
+}
diff --git a/utility/src/main/java/org/oppia/android/util/caching/AssetRepository.kt b/utility/src/main/java/org/oppia/android/util/caching/AssetRepository.kt
index 0e7514e838c..434cc7ee1f0 100644
--- a/utility/src/main/java/org/oppia/android/util/caching/AssetRepository.kt
+++ b/utility/src/main/java/org/oppia/android/util/caching/AssetRepository.kt
@@ -1,190 +1,56 @@
package org.oppia.android.util.caching
-import android.content.Context
import com.google.protobuf.MessageLite
-import org.oppia.android.util.logging.ConsoleLogger
-import java.io.File
-import java.io.InputStream
-import java.io.OutputStream
-import java.net.URL
-import java.util.concurrent.locks.ReentrantLock
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlin.concurrent.withLock
-// TODO(#169): Leverage this repository or a version of it for caching all topic contents in a
-// proto. It may also be worth keeping a version of this repository for caching audio files within
-// certain size limits for buffering during an exploration.
/**
* A generic repository for accessing local APK asset files, and downloading remote binary files.
* This repository aims to centralize caching management of external asset files to simplify
* downstream code, and allow assets to be retrieved quickly and synchronously.
+ *
+ * Implementations of this class can be injected at the application scope and below.
*/
-@Singleton
-class AssetRepository @Inject constructor(
- private val context: Context,
- private val logger: ConsoleLogger
-) {
- private val repositoryLock = ReentrantLock()
-
- /** Map of asset names to file contents for text file assets. */
- private val textFileAssets = mutableMapOf()
-
- /** Map of asset names to file contents for proto file assets. */
- private val protoFileAssets = mutableMapOf()
-
+interface AssetRepository {
/** Returns the whole text contents of the file corresponding to the specified asset name. */
- fun loadTextFileFromLocalAssets(assetName: String): String {
- repositoryLock.withLock {
- primeTextFileFromLocalAssets(assetName)
- return textFileAssets.getValue(assetName)
- }
- }
+ fun loadTextFileFromLocalAssets(assetName: String): String
/**
* Ensures the contents corresponding to the specified asset are available for quick retrieval.
*/
- fun primeTextFileFromLocalAssets(assetName: String) {
- repositoryLock.withLock {
- if (assetName !in textFileAssets) {
- logger.d("AssetRepo", "Caching local text asset: $assetName")
- textFileAssets[assetName] = context.assets.open(assetName).bufferedReader().use {
- it.readText()
- }
- }
- }
- }
+ fun primeTextFileFromLocalAssets(assetName: String)
/**
* Returns a new proto of type [T] that is retrieved from the local assets for the given asset
* name. The [baseMessage] is used to load the proto; its value will never actually be used (so
* callers are recommended to use [T]'s default instance for this purpose).
*/
- fun loadProtoFromLocalAssets(assetName: String, baseMessage: T): T {
- @Suppress("UNCHECKED_CAST") // Safe type-cast per newBuilderForType's contract.
- return baseMessage.newBuilderForType()
- .mergeFrom(loadProtoBlobFromLocalAssets(assetName))
- .build() as T
- }
+ fun loadProtoFromLocalAssets(assetName: String, baseMessage: T): T
- /** Returns the size of the specified proto asset. */
- fun getLocalAssetProtoSize(assetName: String): Int {
- return loadProtoBlobFromLocalAssets(assetName).size
- }
-
- private fun loadProtoBlobFromLocalAssets(assetName: String): ByteArray {
- primeProtoBlobFromLocalAssets(assetName)
- return protoFileAssets.getValue(assetName)
- }
+ /**
+ * A version of [loadProtoFromLocalAssets] which will return the specified default message if the
+ * asset doesn't exist locally (rather than throwing an exception).
+ */
+ fun tryLoadProtoFromLocalAssets(assetName: String, defaultMessage: T): T
- private fun primeProtoBlobFromLocalAssets(assetName: String) {
- repositoryLock.withLock {
- if (assetName !in protoFileAssets) {
- protoFileAssets[assetName] = context.assets.open("$assetName.pb").use { it.readBytes() }
- }
- }
- }
+ /** Returns the size of the specified proto asset, or -1 if the asset doesn't exist. */
+ fun getLocalAssetProtoSize(assetName: String): Int
/**
* Returns a function to retrieve the stream of the binary asset corresponding to the specified
* URL, to be called on a background thread.
*/
- fun loadRemoteBinaryAsset(url: String): () -> ByteArray {
- return {
- logger.d("AssetRepo", "Loading binary asset: $url")
- val stream = openLocalCacheFileForRead(url) ?: openCachingStreamToRemoteFile(url)
- stream.use { it.readBytes() }
- }
- }
+ fun loadRemoteBinaryAsset(url: String): () -> ByteArray
/**
* Returns a function to retrieve the image data corresponding to the specified URL (where the
* image represented by that URL is assumed to be included in the app's assets directory).
*/
- fun loadImageAssetFromLocalAssets(url: String): () -> ByteArray {
- return {
- val filename = url.substring(url.lastIndexOf('/') + 1)
- context.assets.open("images/$filename").use { it.readBytes() }
- }
- }
+ fun loadImageAssetFromLocalAssets(url: String): () -> ByteArray
/** Ensures the contents corresponding to the specified URL are available for quick retrieval. */
- fun primeRemoteBinaryAsset(url: String) {
- if (!isRemoteBinaryAssetDownloaded(url)) {
- // Otherwise, download it remotely and cache it locally.
- logger.d("AssetRepo", "Downloading binary asset: $url")
- val contents = openRemoteStream(url).use { it.readBytes() }
- saveLocalCacheFile(url, contents)
- }
- }
+ fun primeRemoteBinaryAsset(url: String)
/**
* Returns whether a binary asset corresponding to the specified URL has already been downloaded.
*/
- fun isRemoteBinaryAssetDownloaded(url: String): Boolean {
- return getLocalCacheFile(url).exists()
- }
-
- private fun openRemoteStream(url: String): InputStream {
- return URL(url).openStream()
- }
-
- /** Returns an [InputStream] that also saves its results to a local file. */
- private fun openCachingStreamToRemoteFile(url: String): InputStream {
- val urlInStream = openRemoteStream(url)
- val fileOutStream = openLocalCacheFileForWrite(url)
- return object : InputStream() {
- override fun available(): Int {
- return urlInStream.available()
- }
-
- override fun read(): Int {
- val byte = urlInStream.read()
- if (byte != -1) {
- fileOutStream.write(byte)
- }
- return byte
- }
-
- override fun read(b: ByteArray?): Int {
- return read(b, 0, b!!.size)
- }
-
- override fun read(b: ByteArray?, off: Int, len: Int): Int {
- val count = urlInStream.read(b, off, len)
- if (count > -1) {
- fileOutStream.write(b, off, count)
- }
- return count
- }
-
- override fun close() {
- super.close()
- fileOutStream.flush()
- fileOutStream.close()
- urlInStream.close()
- }
- }
- }
-
- private fun openLocalCacheFileForRead(identifier: String): InputStream? {
- val cacheFile = getLocalCacheFile(identifier)
- return if (cacheFile.exists()) cacheFile.inputStream() else null
- }
-
- private fun saveLocalCacheFile(identifier: String, contents: ByteArray) {
- getLocalCacheFile(identifier).writeBytes(contents)
- }
-
- private fun openLocalCacheFileForWrite(identifier: String): OutputStream {
- return getLocalCacheFile(identifier).outputStream()
- }
-
- private fun getLocalCacheFile(identifier: String): File {
- return File(context.cacheDir, convertIdentifierToCacheFileName(identifier))
- }
-
- private fun convertIdentifierToCacheFileName(identifier: String): String {
- return "${identifier.hashCode()}.cache"
- }
+ fun isRemoteBinaryAssetDownloaded(url: String): Boolean
}
diff --git a/utility/src/main/java/org/oppia/android/util/caching/AssetRepositoryImpl.kt b/utility/src/main/java/org/oppia/android/util/caching/AssetRepositoryImpl.kt
new file mode 100644
index 00000000000..ad36216bab9
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/caching/AssetRepositoryImpl.kt
@@ -0,0 +1,191 @@
+package org.oppia.android.util.caching
+
+import android.content.Context
+import com.google.protobuf.MessageLite
+import org.oppia.android.util.logging.ConsoleLogger
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.InputStream
+import java.io.OutputStream
+import java.net.URL
+import java.util.concurrent.locks.ReentrantLock
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.concurrent.withLock
+
+// TODO(#169): Leverage this repository or a version of it for caching all topic contents in a
+// proto. It may also be worth keeping a version of this repository for caching audio files within
+// certain size limits for buffering during an exploration.
+/** Implementation of [AssetRepository]. */
+@Singleton
+class AssetRepositoryImpl @Inject constructor(
+ private val context: Context,
+ private val logger: ConsoleLogger
+) : AssetRepository {
+ private val repositoryLock = ReentrantLock()
+
+ /** Map of asset names to file contents for text file assets. */
+ private val textFileAssets = mutableMapOf()
+
+ /** Map of asset names to file contents for proto file assets. */
+ private val protoFileAssets = mutableMapOf()
+
+ override fun loadTextFileFromLocalAssets(assetName: String): String {
+ repositoryLock.withLock {
+ primeTextFileFromLocalAssets(assetName)
+ return textFileAssets.getValue(assetName)
+ }
+ }
+
+ override fun primeTextFileFromLocalAssets(assetName: String) {
+ repositoryLock.withLock {
+ if (assetName !in textFileAssets) {
+ logger.d("AssetRepo", "Caching local text asset: $assetName")
+ try {
+ textFileAssets[assetName] = context.assets.open(assetName).bufferedReader().use {
+ it.readText()
+ }
+ } catch (e: FileNotFoundException) {
+ // Catch & rethrow for consistency with the proto asset codepath.
+ error("Asset doesn't exist: $assetName")
+ }
+ }
+ }
+ }
+
+ override fun loadProtoFromLocalAssets(assetName: String, baseMessage: T): T {
+ return maybeProtoFromLocalAssetsOrFail(assetName, baseMessage)
+ ?: error("Asset doesn't exist: $assetName")
+ }
+
+ override fun tryLoadProtoFromLocalAssets(
+ assetName: String,
+ defaultMessage: T
+ ): T {
+ return maybeProtoFromLocalAssetsOrFail(assetName, defaultMessage) ?: defaultMessage
+ }
+
+ override fun getLocalAssetProtoSize(assetName: String): Int {
+ return loadProtoBlobFromLocalAssets(assetName)?.size ?: -1
+ }
+
+ private fun maybeProtoFromLocalAssetsOrFail(
+ assetName: String,
+ baseMessage: T
+ ): T? {
+ return loadProtoBlobFromLocalAssets(assetName)?.let { serializedProto ->
+ @Suppress("UNCHECKED_CAST") // Safe type-cast per newBuilderForType's contract.
+ return baseMessage.newBuilderForType()
+ .mergeFrom(serializedProto)
+ .build() as T
+ }
+ }
+
+ private fun loadProtoBlobFromLocalAssets(assetName: String): ByteArray? {
+ primeProtoBlobFromLocalAssets(assetName)
+ return protoFileAssets.getValue(assetName)
+ }
+
+ private fun primeProtoBlobFromLocalAssets(assetName: String) {
+ repositoryLock.withLock {
+ if (assetName !in protoFileAssets) {
+ val files = context.assets.list(/* path= */ "")?.toList() ?: listOf()
+ val assetNameFile = "$assetName.pb"
+ protoFileAssets[assetName] = if (assetNameFile in files) {
+ context.assets.open(assetNameFile).use { it.readBytes() }
+ } else null
+ }
+ }
+ }
+
+ override fun loadRemoteBinaryAsset(url: String): () -> ByteArray {
+ return {
+ logger.d("AssetRepo", "Loading binary asset: $url")
+ val stream = openLocalCacheFileForRead(url) ?: openCachingStreamToRemoteFile(url)
+ stream.use { it.readBytes() }
+ }
+ }
+
+ override fun loadImageAssetFromLocalAssets(url: String): () -> ByteArray {
+ return {
+ val filename = url.substringAfterLast('/')
+ context.assets.open("images/$filename").use { it.readBytes() }
+ }
+ }
+
+ override fun primeRemoteBinaryAsset(url: String) {
+ if (!isRemoteBinaryAssetDownloaded(url)) {
+ // Otherwise, download it remotely and cache it locally.
+ logger.d("AssetRepo", "Downloading binary asset: $url")
+ val contents = openRemoteStream(url).use { it.readBytes() }
+ saveLocalCacheFile(url, contents)
+ }
+ }
+
+ override fun isRemoteBinaryAssetDownloaded(url: String): Boolean {
+ return getLocalCacheFile(url).exists()
+ }
+
+ private fun openRemoteStream(url: String): InputStream {
+ return URL(url).openStream()
+ }
+
+ /** Returns an [InputStream] that also saves its results to a local file. */
+ private fun openCachingStreamToRemoteFile(url: String): InputStream {
+ val urlInStream = openRemoteStream(url)
+ val fileOutStream = openLocalCacheFileForWrite(url)
+ return object : InputStream() {
+ override fun available(): Int {
+ return urlInStream.available()
+ }
+
+ override fun read(): Int {
+ val byte = urlInStream.read()
+ if (byte != -1) {
+ fileOutStream.write(byte)
+ }
+ return byte
+ }
+
+ override fun read(b: ByteArray?): Int {
+ return read(b, 0, b!!.size)
+ }
+
+ override fun read(b: ByteArray?, off: Int, len: Int): Int {
+ val count = urlInStream.read(b, off, len)
+ if (count > -1) {
+ fileOutStream.write(b, off, count)
+ }
+ return count
+ }
+
+ override fun close() {
+ super.close()
+ fileOutStream.flush()
+ fileOutStream.close()
+ urlInStream.close()
+ }
+ }
+ }
+
+ private fun openLocalCacheFileForRead(identifier: String): InputStream? {
+ val cacheFile = getLocalCacheFile(identifier)
+ return if (cacheFile.exists()) cacheFile.inputStream() else null
+ }
+
+ private fun saveLocalCacheFile(identifier: String, contents: ByteArray) {
+ getLocalCacheFile(identifier).writeBytes(contents)
+ }
+
+ private fun openLocalCacheFileForWrite(identifier: String): OutputStream {
+ return getLocalCacheFile(identifier).outputStream()
+ }
+
+ private fun getLocalCacheFile(identifier: String): File {
+ return File(context.cacheDir, convertIdentifierToCacheFileName(identifier))
+ }
+
+ private fun convertIdentifierToCacheFileName(identifier: String): String {
+ return "${identifier.hashCode()}.cache"
+ }
+}
diff --git a/utility/src/main/java/org/oppia/android/util/caching/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/caching/BUILD.bazel
index f665291fa39..46da552daa2 100644
--- a/utility/src/main/java/org/oppia/android/util/caching/BUILD.bazel
+++ b/utility/src/main/java/org/oppia/android/util/caching/BUILD.bazel
@@ -6,34 +6,68 @@ load("@dagger//:workspace_defs.bzl", "dagger_rules")
load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
kt_android_library(
- name = "assets",
+ name = "asset_repository",
srcs = [
"AssetRepository.kt",
+ ],
+ visibility = ["//:oppia_api_visibility"],
+ deps = [
+ "//third_party:com_google_protobuf_protobuf-javalite",
+ ],
+)
+
+kt_android_library(
+ name = "annotations",
+ srcs = [
"CacheAssetsLocally.kt",
"LoadImagesFromAssets.kt",
"LoadLessonProtosFromAssets.kt",
],
visibility = ["//:oppia_api_visibility"],
deps = [
- "//third_party:com_google_protobuf_protobuf-javalite",
"//third_party:javax_inject_javax_inject",
+ ],
+)
+
+kt_android_library(
+ name = "impl",
+ srcs = [
+ "AssetRepositoryImpl.kt",
+ ],
+ deps = [
+ ":asset_repository",
+ ":dagger",
+ "//third_party:com_google_protobuf_protobuf-javalite",
"//utility/src/main/java/org/oppia/android/util/logging:console_logger",
],
)
kt_android_library(
- name = "caching_module",
+ name = "caching_prod_module",
srcs = [
"CachingModule.kt",
],
visibility = ["//:oppia_api_visibility"],
deps = [
- ":assets",
+ ":annotations",
+ ":asset_repository",
":dagger",
":topic_list_to_cache",
],
)
+kt_android_library(
+ name = "asset_prod_module",
+ srcs = [
+ "AssetModule.kt",
+ ],
+ visibility = ["//:oppia_api_visibility"],
+ deps = [
+ ":dagger",
+ ":impl",
+ ],
+)
+
kt_android_library(
name = "topic_list_to_cache",
srcs = [
diff --git a/utility/src/main/java/org/oppia/android/util/caching/testing/AssetTestNoOpModule.kt b/utility/src/main/java/org/oppia/android/util/caching/testing/AssetTestNoOpModule.kt
new file mode 100644
index 00000000000..e46ccd83a42
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/caching/testing/AssetTestNoOpModule.kt
@@ -0,0 +1,12 @@
+package org.oppia.android.util.caching.testing
+
+import dagger.Module
+import dagger.Provides
+import org.oppia.android.util.caching.AssetRepository
+
+/** Test-only module for no-op loading assets. */
+@Module
+class AssetTestNoOpModule {
+ @Provides
+ fun provideAssetRepository(impl: TestNoOpAssetRepository): AssetRepository = impl
+}
diff --git a/utility/src/main/java/org/oppia/android/util/caching/testing/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/caching/testing/BUILD.bazel
index 107ab22fbe4..9663a7c5bef 100644
--- a/utility/src/main/java/org/oppia/android/util/caching/testing/BUILD.bazel
+++ b/utility/src/main/java/org/oppia/android/util/caching/testing/BUILD.bazel
@@ -1,5 +1,5 @@
"""
-Utilities corresponding to caching tests.
+Testing utilities corresponding asset caching utilities.
"""
load("@dagger//:workspace_defs.bzl", "dagger_rules")
@@ -7,15 +7,42 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
kt_android_library(
name = "caching_test_module",
+ testonly = True,
srcs = [
"CachingTestModule.kt",
],
visibility = ["//:oppia_testing_visibility"],
deps = [
":dagger",
- "//utility/src/main/java/org/oppia/android/util/caching:assets",
+ "//utility/src/main/java/org/oppia/android/util/caching:annotations",
"//utility/src/main/java/org/oppia/android/util/caching:topic_list_to_cache",
],
)
+kt_android_library(
+ name = "asset_test_no_op_module",
+ testonly = True,
+ srcs = [
+ "AssetTestNoOpModule.kt",
+ ],
+ visibility = ["//:oppia_testing_visibility"],
+ deps = [
+ ":dagger",
+ ":test_asset_repository",
+ ],
+)
+
+kt_android_library(
+ name = "test_asset_repository",
+ testonly = True,
+ srcs = [
+ "TestNoOpAssetRepository.kt",
+ ],
+ visibility = ["//:oppia_testing_visibility"],
+ deps = [
+ ":dagger",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
+ ],
+)
+
dagger_rules()
diff --git a/utility/src/main/java/org/oppia/android/util/caching/testing/TestNoOpAssetRepository.kt b/utility/src/main/java/org/oppia/android/util/caching/testing/TestNoOpAssetRepository.kt
new file mode 100644
index 00000000000..9cfdc1761bf
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/caching/testing/TestNoOpAssetRepository.kt
@@ -0,0 +1,48 @@
+package org.oppia.android.util.caching.testing
+
+import com.google.protobuf.MessageLite
+import org.oppia.android.util.caching.AssetRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Test-only implementation of [AssetRepository] that provides no support for loading any assets
+ * (i.e. it sets up the environment as though there are no local or remote assets available).
+ *
+ * This class is safe to interact with across multiple simultaneous threads.
+ */
+@Singleton
+class TestNoOpAssetRepository @Inject constructor() : AssetRepository {
+ override fun loadTextFileFromLocalAssets(assetName: String): String {
+ error("Local text asset doesn't exist: $assetName")
+ }
+
+ override fun primeTextFileFromLocalAssets(assetName: String) {
+ // Do nothing.
+ }
+
+ override fun loadProtoFromLocalAssets(assetName: String, baseMessage: T): T {
+ error("Local proto asset doesn't exist: $assetName")
+ }
+
+ override fun tryLoadProtoFromLocalAssets(
+ assetName: String,
+ defaultMessage: T
+ ): T = defaultMessage // Just return default since the asset doesn't exist.
+
+ override fun getLocalAssetProtoSize(assetName: String): Int = -1 // Asset doesn't exist.
+
+ override fun loadRemoteBinaryAsset(url: String): () -> ByteArray {
+ error("Remote asset doesn't exist: $url")
+ }
+
+ override fun loadImageAssetFromLocalAssets(url: String): () -> ByteArray {
+ error("Local image asset doesn't exist: $url")
+ }
+
+ override fun primeRemoteBinaryAsset(url: String) {
+ // Do nothing.
+ }
+
+ override fun isRemoteBinaryAssetDownloaded(url: String): Boolean = false // No local assets exist.
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt
new file mode 100644
index 00000000000..b7e177b25c5
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt
@@ -0,0 +1,112 @@
+package org.oppia.android.util.locale
+
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
+import org.oppia.android.app.model.RegionSupportDefinition
+import java.util.Locale
+
+/**
+ * A profile to represent an Android [Locale] object which can be used to easily compare different
+ * locales (based on the properties the app cares about), or reconstruct a [Locale] object.
+ *
+ * @property languageCode the IETF BCP 47 or ISO 639-2 language code
+ * @property regionCode the IETF BCP 47 or ISO 3166 alpha-2 region code
+ */
+data class AndroidLocaleProfile(val languageCode: String, val regionCode: String) {
+ /** Returns whether this profile matches the specified [otherProfile] for the given locale. */
+ fun matches(
+ machineLocale: OppiaLocale.MachineLocale,
+ otherProfile: AndroidLocaleProfile,
+ ): Boolean {
+ return machineLocale.run {
+ languageCode.equalsIgnoreCase(otherProfile.languageCode)
+ } && machineLocale.run {
+ val regionsAreEqual = regionCode.equalsIgnoreCase(otherProfile.regionCode)
+ val eitherRegionIsWildcard =
+ regionCode == REGION_WILDCARD || otherProfile.regionCode == REGION_WILDCARD
+ return@run regionsAreEqual || eitherRegionIsWildcard
+ }
+ }
+
+ companion object {
+ /** A wildcard that will match against any region when provided. */
+ const val REGION_WILDCARD = "*"
+
+ /** Returns a new [AndroidLocaleProfile] that represents the specified Android [Locale]. */
+ fun createFrom(androidLocale: Locale): AndroidLocaleProfile =
+ AndroidLocaleProfile(androidLocale.language, androidLocale.country)
+
+ /**
+ * Returns a new [AndroidLocaleProfile] using the IETF BCP 47 tag in the provided [LanguageId].
+ *
+ * This will return null in a number of scenarios:
+ * - If the provided [LanguageId] doesn't have an IETF BCP 47 ID
+ * - If the IETF BCP 47 tag is malformed
+ * - If the provided [RegionSupportDefinition] doesn't have an IETF BCP 47 region ID
+ *
+ * Further, this method will only use the provided [regionDefinition] if the IETF BCP 47
+ * language tag doesn't include a region component. If the [regionDefinition] is null then the
+ * returned [AndroidLocaleProfile] will have a wildcard match against any region (meaning only
+ * the language code needs to match).
+ */
+ fun createFromIetfDefinitions(
+ languageId: LanguageId,
+ regionDefinition: RegionSupportDefinition?
+ ): AndroidLocaleProfile? {
+ if (!languageId.hasIetfBcp47Id()) return null
+ return when {
+ "-" in languageId.ietfBcp47Id.ietfLanguageTag -> {
+ val (languageCode, regionCode) =
+ languageId.ietfBcp47Id.ietfLanguageTag.divide("-") ?: return null
+ maybeConstructProfile(languageCode, regionCode)
+ }
+ regionDefinition != null -> {
+ if (!regionDefinition.hasRegionId()) return null
+ maybeConstructProfile(
+ languageId.ietfBcp47Id.ietfLanguageTag, regionDefinition.regionId.ietfRegionTag
+ )
+ }
+ else -> {
+ maybeConstructProfile(
+ languageId.ietfBcp47Id.ietfLanguageTag, regionCode = "", emptyRegionAsWildcard = true
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns a new [AndroidLocaleProfile] using the macaronic ID in the provided [LanguageId].
+ *
+ * This will return null if the [LanguageId] either doesn't have a macaronic ID defined, or if
+ * it's malformed. Macaronic IDs are always expected to include language and region components,
+ * so both fields are guaranteed to be populated in a returned [AndroidLocaleProfile].
+ */
+ fun createFromMacaronicLanguage(
+ languageId: LanguageId
+ ): AndroidLocaleProfile? {
+ if (!languageId.hasMacaronicId()) return null
+ val (languageCode, regionCode) =
+ languageId.macaronicId.combinedLanguageCode.divide("-") ?: return null
+ return maybeConstructProfile(languageCode, regionCode)
+ }
+
+ private fun maybeConstructProfile(
+ languageCode: String,
+ regionCode: String,
+ emptyRegionAsWildcard: Boolean = false
+ ): AndroidLocaleProfile? {
+ return if (languageCode.isNotEmpty() && (regionCode.isNotEmpty() || emptyRegionAsWildcard)) {
+ val adjustedRegionCode = if (emptyRegionAsWildcard && regionCode.isEmpty()) {
+ REGION_WILDCARD
+ } else regionCode
+ AndroidLocaleProfile(languageCode, adjustedRegionCode)
+ } else null
+ }
+
+ private fun String.divide(delimiter: String): Pair? {
+ val results = split(delimiter)
+ return if (results.size == 2) {
+ results[0] to results[1]
+ } else null
+ }
+ }
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel
new file mode 100644
index 00000000000..ce75c3997cb
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel
@@ -0,0 +1,82 @@
+"""
+Generic utilities for managing languages & locales.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
+
+kt_android_library(
+ name = "android_locale_profile",
+ srcs = [
+ "AndroidLocaleProfile.kt",
+ ],
+ visibility = ["//:oppia_api_visibility"],
+ deps = [
+ ":oppia_locale",
+ ],
+)
+
+kt_android_library(
+ name = "oppia_bidi_formatter",
+ srcs = [
+ "OppiaBidiFormatter.kt",
+ ],
+ visibility = [
+ "//domain/src/main/java/org/oppia/android/domain/locale:__pkg__",
+ "//utility:utility_testing_visibility",
+ "//utility/src/main/java/org/oppia/android/util/locale/testing:__pkg__",
+ ],
+)
+
+kt_android_library(
+ name = "oppia_locale",
+ srcs = [
+ "OppiaLocale.kt",
+ ],
+ visibility = ["//:oppia_api_visibility"],
+ deps = [
+ ":oppia_locale_context_extensions",
+ "//model:languages_java_proto_lite",
+ "//third_party:androidx_annotation_annotation",
+ ],
+)
+
+kt_android_library(
+ name = "impl",
+ srcs = [
+ "MachineLocaleImpl.kt",
+ "OppiaBidiFormatterImpl.kt",
+ ],
+ visibility = ["//utility/src/main/java/org/oppia/android/util/locale/testing:__pkg__"],
+ deps = [
+ ":dagger",
+ ":oppia_bidi_formatter",
+ ":oppia_locale",
+ "//utility/src/main/java/org/oppia/android/util/system:oppia_clock",
+ ],
+)
+
+kt_android_library(
+ name = "oppia_locale_context_extensions",
+ srcs = [
+ "OppiaLocaleContextExtensions.kt",
+ ],
+ deps = [
+ "//model:languages_java_proto_lite",
+ ],
+)
+
+kt_android_library(
+ name = "prod_module",
+ srcs = [
+ "LocaleProdModule.kt",
+ ],
+ visibility = ["//:oppia_prod_module_visibility"],
+ deps = [
+ ":dagger",
+ ":impl",
+ ":oppia_locale",
+ ],
+)
+
+dagger_rules()
diff --git a/utility/src/main/java/org/oppia/android/util/locale/LocaleProdModule.kt b/utility/src/main/java/org/oppia/android/util/locale/LocaleProdModule.kt
new file mode 100644
index 00000000000..8dc13a401d0
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/LocaleProdModule.kt
@@ -0,0 +1,14 @@
+package org.oppia.android.util.locale
+
+import dagger.Binds
+import dagger.Module
+
+/** Module for providing production implementations of locale utilities. */
+@Module
+interface LocaleProdModule {
+ @Binds
+ fun bindMachineLocale(impl: MachineLocaleImpl): OppiaLocale.MachineLocale
+
+ @Binds
+ fun bindBidiFormatterFactory(impl: OppiaBidiFormatterImpl.FactoryImpl): OppiaBidiFormatter.Factory
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt b/utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt
new file mode 100644
index 00000000000..ff70d87c6b3
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt
@@ -0,0 +1,100 @@
+package org.oppia.android.util.locale
+
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.oppia.android.util.system.OppiaClock
+import java.text.DateFormat
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import javax.inject.Inject
+
+// TODO(#3766): Restrict to be 'internal'.
+/**
+ * Implementation of [OppiaLocale.MachineLocale].
+ *
+ * Note that this implementation is backed by the Android US [Locale] for consistency among
+ * machine-used strings across runtimes. This is per the advice documented here:
+ * https://developer.android.com/reference/java/util/Locale#default_locale and since the Oppia
+ * Android team generally uses the US locale for identifiers & code.
+ */
+class MachineLocaleImpl @Inject constructor(
+ private val oppiaClock: OppiaClock
+) : OppiaLocale.MachineLocale(machineLocaleContext) {
+ private val parsableDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", machineAndroidLocale) }
+ private val timeFormat by lazy {
+ DateFormat.getTimeInstance(DateFormat.MEDIUM, machineAndroidLocale)
+ }
+
+ override fun String.formatForMachines(vararg args: Any?): String =
+ format(machineAndroidLocale, *args)
+
+ override fun String.toMachineLowerCase(): String = toLowerCase(machineAndroidLocale)
+
+ override fun String.toMachineUpperCase(): String = toUpperCase(machineAndroidLocale)
+
+ override fun String.capitalizeForMachines(): String = capitalize(machineAndroidLocale)
+
+ override fun String.decapitalizeForMachines(): String = decapitalize(machineAndroidLocale)
+
+ override fun String.endsWithIgnoreCase(suffix: String): Boolean =
+ toMachineLowerCase().endsWith(suffix.toMachineLowerCase())
+
+ override fun String?.equalsIgnoreCase(other: String?): Boolean =
+ this?.toMachineLowerCase() == other?.toMachineLowerCase()
+
+ override fun getCurrentTimeOfDay(): TimeOfDay {
+ return when (oppiaClock.getCurrentCalendar().get(Calendar.HOUR_OF_DAY)) {
+ in 4..11 -> TimeOfDay.MORNING
+ in 12..16 -> TimeOfDay.AFTERNOON
+ in 0..3, in 17..23 -> TimeOfDay.EVENING
+ else -> TimeOfDay.UNKNOWN
+ }
+ }
+
+ override fun parseOppiaDate(dateString: String): OppiaDate? {
+ val parsedDate = try {
+ parsableDateFormat.parse(dateString)
+ } catch (e: ParseException) {
+ null
+ }
+ return parsedDate?.let { OppiaDateImpl(it, oppiaClock.getCurrentDate()) }
+ }
+
+ override fun computeCurrentTimeString(): String =
+ timeFormat.format(Date(oppiaClock.getCurrentTimeMs()))
+
+ override fun toString(): String = "MachineLocaleImpl[context=$machineLocaleContext]"
+
+ override fun equals(other: Any?): Boolean {
+ return (other as? MachineLocaleImpl)?.let { locale ->
+ localeContext == locale.localeContext
+ } ?: false
+ }
+
+ override fun hashCode(): Int = localeContext.hashCode()
+
+ private class OppiaDateImpl(private val date: Date, private val today: Date) : OppiaDate {
+ override fun isBeforeToday(): Boolean = date.before(today)
+ }
+
+ private companion object {
+ private val machineLocaleContext by lazy {
+ OppiaLocaleContext.newBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ language = OppiaLanguage.ENGLISH
+ }.build()
+ regionDefinition = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.UNITED_STATES
+ }.build()
+ }.build()
+ }
+
+ private val machineAndroidLocale by lazy { Locale.US }
+ }
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatter.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatter.kt
new file mode 100644
index 00000000000..d85f1d93a44
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatter.kt
@@ -0,0 +1,27 @@
+package org.oppia.android.util.locale
+
+import org.oppia.android.util.locale.OppiaBidiFormatter.Factory
+import java.util.Locale
+
+/**
+ * A custom wrapper to Android's bidirectional formatter so that interactions with this class can be
+ * tested.
+ *
+ * Instances of this class are created via its [Factory].
+ *
+ * Note that this class is only meant to be used by select packages, not broadly. Use [OppiaLocale]
+ * for actual locale-safe string formatting.
+ */
+interface OppiaBidiFormatter {
+ /** Wraps the provided text for bidirectional formatting & returns the result. */
+ fun wrapText(unicode: CharSequence): CharSequence
+
+ /**
+ * Factory for creating new [OppiaBidiFormatter]s. This class can be injected at the application
+ * component and below.
+ */
+ interface Factory {
+ /** Returns a new [OppiaBidiFormatter] corresponding to the specified Android locale. */
+ fun createFormatter(locale: Locale): OppiaBidiFormatter
+ }
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatterImpl.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatterImpl.kt
new file mode 100644
index 00000000000..5940339f9cf
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatterImpl.kt
@@ -0,0 +1,18 @@
+package org.oppia.android.util.locale
+
+import android.text.BidiFormatter
+import java.util.Locale
+import javax.inject.Inject
+
+/** Production implementation of [OppiaBidiFormatter]. */
+class OppiaBidiFormatterImpl private constructor(locale: Locale) : OppiaBidiFormatter {
+ private val bidiFormatter by lazy { BidiFormatter.getInstance(locale) }
+
+ override fun wrapText(unicode: CharSequence): CharSequence = bidiFormatter.unicodeWrap(unicode)
+
+ /** Implementation of [OppiaBidiFormatter.Factory]. */
+ class FactoryImpl @Inject constructor() : OppiaBidiFormatter.Factory {
+ override fun createFormatter(locale: Locale): OppiaBidiFormatter =
+ OppiaBidiFormatterImpl(locale)
+ }
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt
new file mode 100644
index 00000000000..b4aa0fa3439
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt
@@ -0,0 +1,353 @@
+package org.oppia.android.util.locale
+
+import android.content.res.Resources
+import androidx.annotation.ArrayRes
+import androidx.annotation.PluralsRes
+import androidx.annotation.StringRes
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaRegion
+
+/**
+ * Represents a locale in the app. This is similar to Android's locale in that all locale-based
+ * operations should go through this class.
+ *
+ * However, it's different in that:
+ * 1. Some locales must be retrieved asynchronously through domain controllers since the validity of
+ * the locale is verified against the app's own language & region configurations
+ * 2. Because of (1), instances of this class are guaranteed to be supported by the app
+ * 3. Different subclasses of the locale exist to serve different purposes (such as processing
+ * strings, or computing user-displayable strings). This is conceptually similar Android's locale
+ * categories introduced in API 24.
+ * 4. All code in the app must use this class for locale-based operations (as backed by enforced
+ * regex CI checks) to ensure locale correctness in different contexts
+ *
+ * See [org.oppia.android.domain.locale.LocaleController] for retrieving instances of this class,
+ * and at its subclasses below for the different locale APIs that are available.
+ *
+ * Finally, all implementations of this class will be hashable & have equals implementations such
+ * different instances of this class will be equivalent so long as they are _functionally_ the same
+ * locale (which generally means that they are of the same class type and have the same
+ * [localeContext]). Further, subclasses will have custom [toString] implementations for improved
+ * logging messaging.
+ */
+sealed class OppiaLocale {
+ /** The deterministic context that represents this exact locale instance. */
+ abstract val localeContext: OppiaLocaleContext
+
+ /**
+ * Returns the [OppiaLanguage] corresponding to this locale. Locale instances can only correspond
+ * to a single language.
+ */
+ fun getCurrentLanguage(): OppiaLanguage = localeContext.languageDefinition.language
+
+ /**
+ * Returns the primary [LanguageId] corresponding to this locale (to be used when making
+ * interoperable decisions about matching a language-specific string or resource).
+ */
+ fun getLanguageId(): LanguageId = localeContext.getLanguageId()
+
+ /**
+ * Returns the fallback [LanguageId] corresponding to this locale which may be used as a
+ * replacement to [getLanguageId] in contexts where the [LanguageId] returned by that method is
+ * incompatible or unavailable.
+ */
+ fun getFallbackLanguageId(): LanguageId = localeContext.getFallbackLanguageId()
+
+ /**
+ * Returns the [OppiaRegion] corresponding to this locale. Note that this region may not always
+ * represent where the user actually lives. Instead, it's a best approximation for the user's
+ * region based on the system-reported locale & the list of officially supported regions by the
+ * app. Further, the user may always change their region based on system settings (which means
+ * it's entirely up to the user's discretion to indicate that the locale-reported region actually
+ * corresponds to their geographical location).
+ *
+ * There are other methods of representing or computing regions that may be more generic and
+ * accurate (such as using region codes for the former and telephony services for the latter), but
+ * the trade-off here is predictability and simpler implementation.
+ */
+ fun getCurrentRegion(): OppiaRegion = localeContext.regionDefinition.region
+
+ abstract override fun equals(other: Any?): Boolean
+
+ abstract override fun hashCode(): Int
+
+ abstract override fun toString(): String
+
+ /**
+ * An [OppiaLocale] that is used only for machine-readable strings and should *never* be used for
+ * human-readable strings (with the exception of logging statements since those are only ever read
+ * by developers). This locale exists solely for string processing. All user-displayable strings
+ * should be processed using [DisplayLocale].
+ *
+ * Implementations of this class guarantee consistency in string formatting across instances of
+ * the app (including app upgrades), and regardless of whatever locale the user has set up on
+ * their system or the particular version of Android or Android device that they're using.
+ *
+ * Unless otherwise noted, all methods of this class will not perform any bidirectional wrapping
+ * or locale-aware case changing/checking.
+ *
+ * Implementations of this class are available for injection starting that the application
+ * component. All code that depends on this locale can also leverage the fake Oppia clock to
+ * influence test-only behavior for time & date specific operations.
+ *
+ * Finally, some of the methods of this class are extension methods which means instances of the
+ * locale should be run in a contextual way to ensure the function receivers are set up correctly,
+ * e.g.:
+ *
+ * ```kt
+ * val stringsAreEqual = machineLocale.run {
+ * string.equalsIgnoreCase(other)
+ * }
+ * ```
+ */
+ abstract class MachineLocale(override val localeContext: OppiaLocaleContext) : OppiaLocale() {
+ /** Returns a formatted version of this string by interpolating the specified arguments. */
+ abstract fun String.formatForMachines(vararg args: Any?): String
+
+ /** Returns the lowercase version of this string. */
+ abstract fun String.toMachineLowerCase(): String
+
+ /** Returns the uppercase version of this string. */
+ abstract fun String.toMachineUpperCase(): String
+
+ /** Returns the capitalized version of this string. */
+ abstract fun String.capitalizeForMachines(): String
+
+ /** Returns the decapitalized version of this string. */
+ abstract fun String.decapitalizeForMachines(): String
+
+ /** Returns whether this string ends with the specified suffix, ignoring case differences. */
+ abstract fun String.endsWithIgnoreCase(suffix: String): Boolean
+
+ /**
+ * Returns whether this string is the same as the specified string, ignoring case differences.
+ */
+ abstract fun String?.equalsIgnoreCase(other: String?): Boolean
+
+ /**
+ * Returns the current [TimeOfDay].
+ *
+ * Note that the returned date object is always corresponding to the local timezone of the
+ * device which may not relate to the user's defined locale.
+ */
+ abstract fun getCurrentTimeOfDay(): TimeOfDay
+
+ /**
+ * Returns an [OppiaDate] object representing the specified date string, or null if the string
+ * is incorrectly formatted. This has the same timezone caveat as [getCurrentTimeOfDay].
+ *
+ * Dates are expected in the format: YYYY-MM-DD.
+ */
+ abstract fun parseOppiaDate(dateString: String): OppiaDate?
+
+ /**
+ * Returns a time string corresponding to the current wall clock time. Note that, as with other
+ * methods in this class, this should never be used for UI-displayed strings. It's intended for
+ * developer-facing strings, instead, such as log statements.
+ *
+ * The returned string is guaranteed to include at least the hour, minute, and second pertaining
+ * to the current wall time (though no guarantees are made to other time information being
+ * included, or in how this information is presented).
+ */
+ abstract fun computeCurrentTimeString(): String
+
+ /** Represents different times of day. */
+ enum class TimeOfDay {
+ /** Corresponds to the user's morning time. */
+ MORNING,
+
+ /** Corresponds to the user's afternoon time. */
+ AFTERNOON,
+
+ /** Corresponds to the user's evening time. */
+ EVENING,
+
+ /**
+ * Corresponds to an unknown time of day (implying that something might have gone wrong during
+ * the time-of-day computation).
+ */
+ UNKNOWN
+ }
+
+ /** An abstract representation of a date. */
+ interface OppiaDate {
+ /**
+ * Returns whether this date occurred before today.
+ *
+ * Note that the internal implementation is not required to guarantee day alignment, so it
+ * must be assigned that this check relates to the time at which the _object itself is
+ * created_ rather than the day on which it was created.
+ */
+ fun isBeforeToday(): Boolean
+ }
+ }
+
+ /**
+ * An [OppiaLocale] which can be used to compute user-displayable strings. Generally, this only
+ * applies to app strings (other content-based localization decisions are handled by
+ * [ContentLocale]).
+ *
+ * Instances of this class must be retrieved through domain layer controllers (generally via data
+ * providers) since the locale object to use may change if the user decides to change which
+ * language that they want to use within the app. Because of this, instances of this class are not
+ * available outside of the app layer.
+ *
+ * This class should only be used for strings that will eventually be shown to the user, or to
+ * make decisions regarding the layout of views shown to the user. Other strings should be
+ * processed using [MachineLocale] to ensure consistency across app instances.
+ *
+ * Note that some of the methods of this class use string receivers. See the documentation for
+ * [MachineLocale] for a code sample on how to wrap this locale in a receiver context to call
+ * those methods.
+ *
+ * Finally, note that resource-based methods do not guarantee that the strings returned are tied
+ * to this locale. Only the active locale can affect the strings returned (see
+ * [org.oppia.android.domain.locale.LocaleController.setAsDefault]).
+ */
+ abstract class DisplayLocale(override val localeContext: OppiaLocaleContext) : OppiaLocale() {
+ /**
+ * Returns a locally formatted date string representing the specified Unix timestamp.
+ *
+ * No assumptions can be made regarding the formatting of the returned string. Further, the
+ * implementation aims to return a string that includes a medium amount of information
+ * corresponding to the time (i.e. 'January 1, 2019').
+ */
+ abstract fun computeDateString(timestampMillis: Long): String
+
+ /**
+ * Returns a locally formatted time string representing the specified Unix timestamp.
+ *
+ * Similar to [computeDateString], no assumptions can be made about the format returned.
+ * Implementations should return enough information to convey hours and minutes (i.e. 4:45pm or
+ * 16:45).
+ */
+ abstract fun computeTimeString(timestampMillis: Long): String
+
+ /**
+ * Returns a locally formatted date/time string representing the specified Unix timestamp.
+ *
+ * Similar to [computeDateString], no assumptions can be made about the format returned. The
+ * information included corresponds to the documented behaviors of [computeDateString] and
+ * [computeTimeString].
+ */
+ abstract fun computeDateTimeString(timestampMillis: Long): String
+
+ /**
+ * Returns the [androidx.core.view.ViewCompat] layout direction that should be used in layouts
+ * that display app strings localized by this locale.
+ */
+ abstract fun getLayoutDirection(): Int
+
+ /**
+ * Returns a formatted version of this string by interpolating the specified arguments, taking
+ * according to this locale.
+ *
+ * This method attempts to ensure bidirectional consistency by wrapping certain arguments with
+ * bidirectional markers so that they appear in the correct position in the returned string. See
+ * https://developer.android.com/training/basics/supporting-devices/languages#FormatTextExplanationSolution
+ * for specific cases when this can occur. Note that the method only supports taking
+ * CharSequences since all other types are likely machine-readable and need corresponding
+ * resource strings to represent their contents (except potentially integers which can either be
+ * directly converted to a string or formatted using [MachineLocale]). Further, null types are
+ * prohibited since 'null' is not a user-readable concept.
+ *
+ * This method should generally only be used for strings that are about to be immediately shown
+ * to the user. For strings that are intermediaries that will be part of other strings shown to
+ * the user, use [formatInLocaleWithoutWrapping] & one of the other 'withWrapping' methods to
+ * ensure the result is wrapped (strings shouldn't need to be wrapped multiple times for
+ * bidirectional correctness).
+ */
+ abstract fun String.formatInLocaleWithWrapping(vararg args: CharSequence): String
+
+ /**
+ * Returns a formatted version of this string by interpolating the specified arguments, taking
+ * according to this locale.
+ *
+ * Unlike [formatInLocaleWithWrapping], this method does not perform any bidirectional wrapping
+ * for the supplied arguments. However, it has the same argument restrictions as
+ * [formatInLocaleWithWrapping] since it's expected to produce strings that are meant to be
+ * eventually shown to the user (it's very likely the returned value by this method will need to
+ * be wrapped for bidirectional correctness by passing it to a subsequent 'withWrapping' method.
+ */
+ abstract fun String.formatInLocaleWithoutWrapping(vararg args: CharSequence): String
+
+ /** Returns a locale-aware capitalized version of this string suitable for displaying in UIs. */
+ abstract fun String.capitalizeForHumans(): String
+
+ /**
+ * Returns the exact resource string from this [Resources] object per the specified string
+ * resource ID.
+ */
+ abstract fun Resources.getStringInLocale(@StringRes id: Int): String
+
+ /**
+ * Returns a formatted string as described in [formatInLocaleWithWrapping] except for a string
+ * format retrieved from this [Resources] object (according to this locale). For this reason,
+ * strings that include non-string parameters are not supported for wrapping. Instead, arguments
+ * should be correctly converted to human-readable strings before being passed to this method.
+ * This also means that the strings themselves should only ever take string types since
+ * non-strings cannot be passed in.
+ */
+ abstract fun Resources.getStringInLocaleWithWrapping(
+ @StringRes id: Int,
+ vararg formatArgs: CharSequence
+ ): String
+
+ /**
+ * Returns a formatted string as described in [formatInLocaleWithoutWrapping] for a resource
+ * string. Unlike [getStringInLocaleWithWrapping], this method does not perform bidirectional
+ * wrapping to passed arguments.
+ */
+ abstract fun Resources.getStringInLocaleWithoutWrapping(
+ @StringRes id: Int,
+ vararg formatArgs: CharSequence
+ ): String
+
+ /** Returns the string array corresponding to the specified ID from this [Resources] object. */
+ abstract fun Resources.getStringArrayInLocale(@ArrayRes id: Int): List
+
+ /**
+ * Returns the quantity string specified ID and for the specified [quantity] from this
+ * [Resources] object.
+ */
+ abstract fun Resources.getQuantityStringInLocale(@PluralsRes id: Int, quantity: Int): String
+
+ /**
+ * Returns a quantity string with formatting per [getQuantityStringInLocale] and
+ * [getStringInLocaleWithWrapping].
+ */
+ abstract fun Resources.getQuantityStringInLocaleWithWrapping(
+ @PluralsRes id: Int,
+ quantity: Int,
+ vararg formatArgs: CharSequence
+ ): String
+
+ /**
+ * Returns a quantity string with formatting per [getQuantityStringInLocale] and
+ * [getStringInLocaleWithoutWrapping].
+ */
+ abstract fun Resources.getQuantityStringInLocaleWithoutWrapping(
+ @PluralsRes id: Int,
+ quantity: Int,
+ vararg formatArgs: CharSequence
+ ): String
+
+ /**
+ * Returns a similar result as [getQuantityStringInLocale] except as a [CharSequence] instead of
+ * as a [String]. See [Resources.getQuantityText] for more details.
+ */
+ abstract fun Resources.getQuantityTextInLocale(@PluralsRes id: Int, quantity: Int): CharSequence
+ }
+
+ /**
+ * An [OppiaLocale] representing content-based localization (such as for written translation
+ * strings in lessons or audio voiceovers.
+ *
+ * This generally doesn't perform operations directly on strings and instead acts as a wrapper for
+ * an [OppiaLocaleContext] which provides details on how to make language-based decisions
+ * corresponding to content localization.
+ */
+ abstract class ContentLocale(override val localeContext: OppiaLocaleContext) : OppiaLocale()
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocaleContextExtensions.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocaleContextExtensions.kt
new file mode 100644
index 00000000000..81135e2375f
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocaleContextExtensions.kt
@@ -0,0 +1,29 @@
+package org.oppia.android.util.locale
+
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.UNRECOGNIZED
+import org.oppia.android.app.model.OppiaLocaleContext.LanguageUsageMode.USAGE_MODE_UNSPECIFIED
+
+/** Returns the primary [LanguageId] corresponding to this context, based on its intended usage. */
+fun OppiaLocaleContext.getLanguageId(): LanguageId {
+ return when (usageMode) {
+ APP_STRINGS -> languageDefinition.appStringId
+ CONTENT_STRINGS -> languageDefinition.contentStringId
+ AUDIO_TRANSLATIONS -> languageDefinition.audioTranslationId
+ USAGE_MODE_UNSPECIFIED, UNRECOGNIZED, null -> LanguageId.getDefaultInstance()
+ }
+}
+
+/** Returns the fallback [LanguageId] corresponding to this context, based on its intended usage. */
+fun OppiaLocaleContext.getFallbackLanguageId(): LanguageId {
+ return when (usageMode) {
+ APP_STRINGS -> fallbackLanguageDefinition.appStringId
+ CONTENT_STRINGS -> fallbackLanguageDefinition.contentStringId
+ AUDIO_TRANSLATIONS -> fallbackLanguageDefinition.audioTranslationId
+ USAGE_MODE_UNSPECIFIED, UNRECOGNIZED, null -> LanguageId.getDefaultInstance()
+ }
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/testing/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/locale/testing/BUILD.bazel
new file mode 100644
index 00000000000..15136e7c69e
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/testing/BUILD.bazel
@@ -0,0 +1,36 @@
+"""
+Testing utilities for language & locale general utilities.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
+
+kt_android_library(
+ name = "test_oppia_bidi_formatter",
+ testonly = True,
+ srcs = [
+ "TestOppiaBidiFormatter.kt",
+ ],
+ visibility = ["//:oppia_testing_visibility"],
+ deps = [
+ ":dagger",
+ "//utility/src/main/java/org/oppia/android/util/locale:impl",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_bidi_formatter",
+ ],
+)
+
+kt_android_library(
+ name = "test_module",
+ testonly = True,
+ srcs = [
+ "LocaleTestModule.kt",
+ ],
+ visibility = ["//:oppia_testing_visibility"],
+ deps = [
+ ":dagger",
+ ":test_oppia_bidi_formatter",
+ "//utility/src/main/java/org/oppia/android/util/locale:impl",
+ ],
+)
+
+dagger_rules()
diff --git a/utility/src/main/java/org/oppia/android/util/locale/testing/LocaleTestModule.kt b/utility/src/main/java/org/oppia/android/util/locale/testing/LocaleTestModule.kt
new file mode 100644
index 00000000000..1f2e611ba1b
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/testing/LocaleTestModule.kt
@@ -0,0 +1,17 @@
+package org.oppia.android.util.locale.testing
+
+import dagger.Binds
+import dagger.Module
+import org.oppia.android.util.locale.MachineLocaleImpl
+import org.oppia.android.util.locale.OppiaBidiFormatter
+import org.oppia.android.util.locale.OppiaLocale
+
+/** Module for providing testing implementations of locale utilities. */
+@Module
+interface LocaleTestModule {
+ @Binds
+ fun bindMachineLocale(impl: MachineLocaleImpl): OppiaLocale.MachineLocale
+
+ @Binds
+ fun bindBidiFormatterFactory(impl: TestOppiaBidiFormatter.FactoryImpl): OppiaBidiFormatter.Factory
+}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/testing/TestOppiaBidiFormatter.kt b/utility/src/main/java/org/oppia/android/util/locale/testing/TestOppiaBidiFormatter.kt
new file mode 100644
index 00000000000..210ac897bbf
--- /dev/null
+++ b/utility/src/main/java/org/oppia/android/util/locale/testing/TestOppiaBidiFormatter.kt
@@ -0,0 +1,89 @@
+package org.oppia.android.util.locale.testing
+
+import org.oppia.android.util.locale.OppiaBidiFormatter
+import org.oppia.android.util.locale.OppiaBidiFormatterImpl
+import org.oppia.android.util.locale.testing.TestOppiaBidiFormatter.Checker
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Test-only implementation of [OppiaBidiFormatter] for verifying calls to the formatter.
+ *
+ * See [Checker.isTextWrapped] for details on how to validate string wrapping. Further, note that
+ * this formatter delegates to the production formatter to ensure that wrapping still happens, and
+ * it will validate that strings are not wrapped multiple times.
+ *
+ * Implementations of this class are available by injecting [OppiaBidiFormatter.Factory] at the test
+ * scope & constructing a new formatter.
+ */
+class TestOppiaBidiFormatter private constructor(
+ private val prodFormatter: OppiaBidiFormatter,
+ private val checker: Checker
+) : OppiaBidiFormatter {
+ override fun wrapText(unicode: CharSequence): CharSequence {
+ check(unicode !is WrappedStringMarker) {
+ "Error: encountered string that's already been wrapped: $unicode"
+ }
+ checker.wrappedTexts += unicode
+ return WrappedStringMarker(prodFormatter.wrapText(unicode))
+ }
+
+ /** Implementation of [OppiaBidiFormatter.Factory] for test formatters. */
+ class FactoryImpl @Inject constructor(
+ private val prodFactoryImpl: OppiaBidiFormatterImpl.FactoryImpl,
+ private val checker: Checker
+ ) : OppiaBidiFormatter.Factory {
+ override fun createFormatter(locale: Locale): OppiaBidiFormatter =
+ TestOppiaBidiFormatter(prodFactoryImpl.createFormatter(locale), checker)
+ }
+
+ /**
+ * Checker utility for determining whether a [CharSequence] has been wrapped for bidirectional
+ * formatting.
+ *
+ * This class can be injected at the test application scope & be used for any strings wrapped by
+ * any [TestOppiaBidiFormatter]s.
+ */
+ @Singleton
+ class Checker @Inject constructor() {
+ /** The list of texts wrapped using [wrapText] across all formatters. */
+ internal val wrappedTexts = mutableListOf()
+
+ /**
+ * Returns whether the specified unicode sequence has been wrapped for formatting by this class
+ * (i.e. by a call to [wrapText]).
+ *
+ * Note that [wrapText] will ensure that already wrapped sequences are not wrapped again.
+ * Further, certain operations will break this check (particularly, conversion to a string or
+ * anything that returns a different char scequence from the original).
+ *
+ * Finally, this method will not return true for a string formatted by the original production
+ * implementation or Android's bidirectional formatter directly. This is only meant to be used
+ * on char sequences returned recently from a call to [wrapText].
+ */
+ fun isTextWrapped(unicode: CharSequence): Boolean = unicode is WrappedStringMarker
+
+ /**
+ * Returns the list of all sequences wrapped using [wrapText] across all formatters. Care should
+ * be taken if multiple formatters are used in succession since this list does not necessarily
+ * guarantee order.
+ */
+ fun getAllWrappedUnicodeTexts(): List = wrappedTexts
+ }
+
+ private class WrappedStringMarker(private val wrappedSeq: CharSequence) : CharSequence {
+ override val length: Int = wrappedSeq.length
+
+ override fun get(index: Int): Char = wrappedSeq[index]
+
+ override fun subSequence(startIndex: Int, endIndex: Int): CharSequence =
+ wrappedSeq.subSequence(startIndex, endIndex)
+
+ override fun toString(): String = wrappedSeq.toString()
+
+ override fun hashCode(): Int = wrappedSeq.hashCode()
+
+ override fun equals(other: Any?): Boolean = toString() == other
+ }
+}
diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt b/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt
index 03b7a1415de..b11652e0207 100644
--- a/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt
+++ b/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt
@@ -5,7 +5,6 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
-import org.oppia.android.util.caching.AssetRepository
import org.oppia.android.util.parser.svg.BlockPictureDrawable
import org.oppia.android.util.parser.svg.BlockSvgDrawableTranscoder
import org.oppia.android.util.parser.svg.ScalableVectorGraphic
@@ -13,7 +12,10 @@ import org.oppia.android.util.parser.svg.SvgDecoder
import org.oppia.android.util.parser.svg.TextSvgDrawableTranscoder
import java.io.InputStream
-/** Custom [AppGlideModule] to enable loading images from [AssetRepository] via Glide. */
+/**
+ * Custom [AppGlideModule] to enable loading images from
+ * [org.oppia.android.util.caching.AssetRepository] via Glide.
+ */
@GlideModule
class RepositoryGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
diff --git a/utility/src/main/java/org/oppia/android/util/system/OppiaClock.kt b/utility/src/main/java/org/oppia/android/util/system/OppiaClock.kt
index a1a7902a07f..88d8d0d541b 100644
--- a/utility/src/main/java/org/oppia/android/util/system/OppiaClock.kt
+++ b/utility/src/main/java/org/oppia/android/util/system/OppiaClock.kt
@@ -1,6 +1,7 @@
package org.oppia.android.util.system
import java.util.Calendar
+import java.util.Date
/** Utility to get the current date/time. Tests should use the fake version of this class. */
interface OppiaClock {
@@ -18,5 +19,13 @@ interface OppiaClock {
* Returns the current date and time as a [Calendar]. Unlike [getCurrentTimeMs], the returned
* [Calendar] takes into account the user's local time zone.
*/
- fun getCurrentCalendar(): Calendar
+ fun getCurrentCalendar(): Calendar = Calendar.getInstance().apply {
+ timeInMillis = getCurrentTimeMs()
+ }
+
+ /**
+ * Returns the [Date] corresponding to the current instant in time, according to
+ * [getCurrentTimeMs].
+ */
+ fun getCurrentDate(): Date = Date(getCurrentTimeMs())
}
diff --git a/utility/src/main/java/org/oppia/android/util/system/OppiaClockImpl.kt b/utility/src/main/java/org/oppia/android/util/system/OppiaClockImpl.kt
index ab7a331e806..4c73b311447 100644
--- a/utility/src/main/java/org/oppia/android/util/system/OppiaClockImpl.kt
+++ b/utility/src/main/java/org/oppia/android/util/system/OppiaClockImpl.kt
@@ -1,15 +1,8 @@
package org.oppia.android.util.system
-import java.util.Calendar
import javax.inject.Inject
/** Implementation of [OppiaClock] that uses real time dependencies. */
class OppiaClockImpl @Inject constructor() : OppiaClock {
override fun getCurrentTimeMs(): Long = System.currentTimeMillis()
-
- override fun getCurrentCalendar(): Calendar {
- val calendar = Calendar.getInstance()
- calendar.timeInMillis = getCurrentTimeMs()
- return calendar
- }
}
diff --git a/utility/src/test/java/org/oppia/android/util/caching/AssetModuleTest.kt b/utility/src/test/java/org/oppia/android/util/caching/AssetModuleTest.kt
new file mode 100644
index 00000000000..a962b41fa77
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/caching/AssetModuleTest.kt
@@ -0,0 +1,79 @@
+package org.oppia.android.util.caching
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.util.logging.LoggerModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [AssetModule]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class AssetModuleTest {
+ @Inject
+ lateinit var assetRepository: AssetRepository
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testModule_injectsProductionImplementationOfAssetRepository() {
+ assertThat(assetRepository).isInstanceOf(AssetRepositoryImpl::class.java)
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerAssetModuleTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, AssetModule::class, LoggerModule::class, TestDispatcherModule::class,
+ RobolectricModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(assetModuleTest: AssetModuleTest)
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/caching/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/caching/BUILD.bazel
new file mode 100644
index 00000000000..41366f342d3
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/caching/BUILD.bazel
@@ -0,0 +1,29 @@
+"""
+Tests for caching utilities.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "AssetModuleTest",
+ srcs = ["AssetModuleTest.kt"],
+ custom_package = "org.oppia.android.util.caching",
+ test_class = "org.oppia.android.util.caching.AssetModuleTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
+ "//testing/src/main/java/org/oppia/android/testing/threading:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
+ "//utility/src/main/java/org/oppia/android/util/logging:prod_module",
+ ],
+)
+
+dagger_rules()
diff --git a/utility/src/test/java/org/oppia/android/util/caching/testing/AssetTestNoOpModuleTest.kt b/utility/src/test/java/org/oppia/android/util/caching/testing/AssetTestNoOpModuleTest.kt
new file mode 100644
index 00000000000..d577afad117
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/caching/testing/AssetTestNoOpModuleTest.kt
@@ -0,0 +1,76 @@
+package org.oppia.android.util.caching.testing
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.util.caching.AssetRepository
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [AssetTestNoOpModule]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class AssetTestNoOpModuleTest {
+ @Inject
+ lateinit var assetRepository: AssetRepository
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testModule_injectsTestImplementationOfAssetRepository() {
+ assertThat(assetRepository).isInstanceOf(TestNoOpAssetRepository::class.java)
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerAssetTestNoOpModuleTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, AssetTestNoOpModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(assetTestNoOpModuleTest: AssetTestNoOpModuleTest)
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel
new file mode 100644
index 00000000000..a4187f0dede
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel
@@ -0,0 +1,47 @@
+"""
+Tests for testing utilities corresponding asset caching utilities.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "AssetTestNoOpModuleTest",
+ srcs = ["AssetTestNoOpModuleTest.kt"],
+ custom_package = "org.oppia.android.util.caching.testing",
+ test_class = "org.oppia.android.util.caching.testing.AssetTestNoOpModuleTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
+ "//utility/src/main/java/org/oppia/android/util/caching/testing:asset_test_no_op_module",
+ ],
+)
+
+oppia_android_test(
+ name = "TestNoOpAssetRepositoryTest",
+ srcs = ["TestNoOpAssetRepositoryTest.kt"],
+ custom_package = "org.oppia.android.util.caching.testing",
+ test_class = "org.oppia.android.util.caching.testing.TestNoOpAssetRepositoryTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//model:test_models",
+ "//testing",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_extensions_truth-liteproto-extension",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/caching:asset_repository",
+ "//utility/src/main/java/org/oppia/android/util/caching/testing:asset_test_no_op_module",
+ ],
+)
+
+dagger_rules()
diff --git a/utility/src/test/java/org/oppia/android/util/caching/testing/TestNoOpAssetRepositoryTest.kt b/utility/src/test/java/org/oppia/android/util/caching/testing/TestNoOpAssetRepositoryTest.kt
new file mode 100644
index 00000000000..1bc640a5c8e
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/caching/testing/TestNoOpAssetRepositoryTest.kt
@@ -0,0 +1,187 @@
+package org.oppia.android.util.caching.testing
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.TestMessage
+import org.oppia.android.testing.assertThrows
+import org.oppia.android.util.caching.AssetRepository
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [TestNoOpAssetRepository]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class TestNoOpAssetRepositoryTest {
+ @Inject
+ lateinit var assetRepository: AssetRepository
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testLoadTextFileFromLocalAssets_throwsException() {
+ val exception = assertThrows(IllegalStateException::class) {
+ assetRepository.loadTextFileFromLocalAssets("asset.json")
+ }
+
+ assertThat(exception).hasMessageThat().contains("Local text asset doesn't exist: asset.json")
+ }
+
+ @Test
+ fun testPrimeTextFileFromLocalAssets_doesNotThrowException() {
+ assetRepository.primeTextFileFromLocalAssets("asset.json")
+
+ // Nothing happens since priming no-ops.
+ }
+
+ @Test
+ fun testPrimeTextFileFromLocalAssets_thenLoadAsset_throwsException() {
+ assetRepository.primeTextFileFromLocalAssets("asset.json")
+
+ // Priming doesn't do anything, so the exception is still thrown.
+ val exception = assertThrows(IllegalStateException::class) {
+ assetRepository.loadTextFileFromLocalAssets("asset.json")
+ }
+
+ assertThat(exception).hasMessageThat().contains("Local text asset doesn't exist: asset.json")
+ }
+
+ @Test
+ fun testLoadProtoFromLocalAssets_throwsException() {
+ val exception = assertThrows(IllegalStateException::class) {
+ assetRepository.loadProtoFromLocalAssets("test", TestMessage.getDefaultInstance())
+ }
+
+ assertThat(exception).hasMessageThat().contains("Local proto asset doesn't exist: test")
+ }
+
+ @Test
+ fun testTryLoadProtoFromLocalAssets_returnsDefaultProto() {
+ val testMessage = TestMessage.newBuilder().apply {
+ intValue = 12
+ }.build()
+
+ val result = assetRepository.tryLoadProtoFromLocalAssets("test", testMessage)
+
+ // tryLoad() will always return the default message provided since no local assets exist.
+ assertThat(result).isEqualTo(testMessage)
+ }
+
+ @Test
+ fun testGetLocalAssetProtoSize_returnsNegativeOne() {
+ val size = assetRepository.getLocalAssetProtoSize("test")
+
+ // The size is always -1 since no local assets exist.
+ assertThat(size).isEqualTo(-1)
+ }
+
+ @Test
+ fun testLoadRemoteBinaryAsset_throwsException() {
+ val exception = assertThrows(IllegalStateException::class) {
+ assetRepository.loadRemoteBinaryAsset("https://example.com/test.pb")
+ }
+
+ assertThat(exception).hasMessageThat()
+ .contains("Remote asset doesn't exist: https://example.com/test.pb")
+ }
+
+ @Test
+ fun testLoadImageAssetFromLocalAssets_throwsException() {
+ val exception = assertThrows(IllegalStateException::class) {
+ assetRepository.loadImageAssetFromLocalAssets("https://example.com/test.png")
+ }
+
+ assertThat(exception).hasMessageThat()
+ .contains("Local image asset doesn't exist: https://example.com/test.png")
+ }
+
+ @Test
+ fun testPrimeRemoteBinaryAsset_doesNotThrowException() {
+ assetRepository.primeRemoteBinaryAsset("https://example.com/test.pb")
+
+ // Nothing happens since priming no-ops.
+ }
+
+ @Test
+ fun testPrimeRemoteBinaryAsset_thenLoad_throwsException() {
+ assetRepository.primeRemoteBinaryAsset("https://example.com/test.pb")
+
+ // Priming doesn't do anything, so the exception is still thrown.
+ val exception = assertThrows(IllegalStateException::class) {
+ assetRepository.loadRemoteBinaryAsset("https://example.com/test.pb")
+ }
+
+ assertThat(exception).hasMessageThat()
+ .contains("Remote asset doesn't exist: https://example.com/test.pb")
+ }
+
+ @Test
+ fun testIsRemoteBinaryAssetDownloaded_returnsFalse() {
+ val exists = assetRepository.isRemoteBinaryAssetDownloaded("https://example.com/test.pb")
+
+ assertThat(exists).isFalse() // Nothing exists.
+ }
+
+ @Test
+ fun testIsRemoteBinaryAssetDownloaded_afterPriming_returnsFalse() {
+ assetRepository.primeRemoteBinaryAsset("https://example.com/test.pb")
+
+ val exists = assetRepository.isRemoteBinaryAssetDownloaded("https://example.com/test.pb")
+
+ assertThat(exists).isFalse() // Nothing still exists since priming does nothing.
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerTestNoOpAssetRepositoryTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, AssetTestNoOpModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(testNoOpAssetRepositoryTest: TestNoOpAssetRepositoryTest)
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt
new file mode 100644
index 00000000000..6a83b6a051d
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt
@@ -0,0 +1,544 @@
+package org.oppia.android.util.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.LanguageSupportDefinition.IetfBcp47LanguageId
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
+import org.oppia.android.app.model.LanguageSupportDefinition.MacaronicLanguageId
+import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.RegionSupportDefinition
+import org.oppia.android.app.model.RegionSupportDefinition.IetfBcp47RegionId
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [AndroidLocaleProfile]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class AndroidLocaleProfileTest {
+ @Inject
+ lateinit var machineLocale: OppiaLocale.MachineLocale
+
+ private val portugueseLocale by lazy { Locale("pt") }
+ private val brazilianPortugueseLocale by lazy { Locale("pt", "BR") }
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ /* Tests for createFrom */
+
+ @Test
+ fun testCreateProfile_fromRootLocale_returnsProfileWithoutLanguageAndRegionCode() {
+ val profile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+
+ assertThat(profile.languageCode).isEmpty()
+ assertThat(profile.regionCode).isEmpty()
+ }
+
+ @Test
+ fun testCreateProfile_fromEnglishLocale_returnsProfileWithLanguageAndWithoutRegion() {
+ val profile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+
+ assertThat(profile.languageCode).isEqualTo("en")
+ assertThat(profile.regionCode).isEmpty()
+ }
+
+ @Test
+ fun testCreateProfile_fromBrazilianPortuguese_returnsProfileWithLanguageAndRegion() {
+ val profile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ assertThat(profile.languageCode).isEqualTo("pt")
+ assertThat(profile.regionCode).isEqualTo("BR")
+ }
+
+ /* Tests for createFromIetfDefinitions */
+
+ @Test
+ fun testCreateProfileFromIetf_defaultLanguageId_nullRegion_returnsNull() {
+ val profile =
+ AndroidLocaleProfile.createFromIetfDefinitions(
+ languageId = LanguageId.getDefaultInstance(), regionDefinition = null
+ )
+
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_languageIdWithoutIetf_withRegion_returnsNull() {
+ val languageWithoutIetf = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-en"
+ }.build()
+ }.build()
+
+ val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithoutIetf, REGION_INDIA)
+
+ // The language ID needs to have an IETF BCP 47 ID defined.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_languageIdWithEmptyIetf_withRegion_returnsNull() {
+ val languageWithIetf = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = ""
+ }.build()
+ }.build()
+
+ val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+
+ // The language ID needs to have an IETF BCP 47 ID defined.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_languageIdWithMalformedIetf_withRegion_returnsNull() {
+ val languageWithIetf = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "mal-form-ed"
+ }.build()
+ }.build()
+
+ val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+
+ // The language ID needs to have a well-formed IETF BCP 47 ID defined.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_languageIdWithIetfLanguageCode_withRegion_returnsCombinedProfile() {
+ val languageWithIetf = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt"
+ }.build()
+ }.build()
+
+ val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+
+ // The constituent language code should come from the language ID, and the region code from the
+ // provided region definition.
+ assertThat(profile?.languageCode).isEqualTo("pt")
+ assertThat(profile?.regionCode).isEqualTo("IN")
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_withIetfLanguageRegionTag_withRegion_returnsIetfRegionProfile() {
+ val languageWithIetf = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt-BR"
+ }.build()
+ }.build()
+
+ val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+
+ // In this case, the region comes from the IETF language tag since it's included.
+ assertThat(profile?.languageCode).isEqualTo("pt")
+ assertThat(profile?.regionCode).isEqualTo("BR")
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_languageIdWithIetfLanguageCode_withDefaultRegion_returnsNull() {
+ val languageWithIetf = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromIetfDefinitions(
+ languageWithIetf, regionDefinition = RegionSupportDefinition.getDefaultInstance()
+ )
+
+ // The region is needed in this case, so it needs to be provided.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_languageIdWithIetfLanguageCode_withEmptyRegion_returnsNull() {
+ val languageWithIetf = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromIetfDefinitions(
+ languageWithIetf, regionDefinition = RegionSupportDefinition.getDefaultInstance()
+ )
+
+ // The region is needed in this case, so a valid one needs to be provided.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromIetf_withIetfLanguageCode_withNullRegion_returnsWildcardProfile() {
+ val languageWithIetf = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, regionDefinition = null)
+
+ // A null region specifically means to use a wildcard match for regions.
+ assertThat(profile?.languageCode).isEqualTo("pt")
+ assertThat(profile?.regionCode).isEqualTo(AndroidLocaleProfile.REGION_WILDCARD)
+ }
+
+ /* Tests for createFromMacaronicLanguage */
+
+ @Test
+ fun testCreateProfileFromMacaronic_defaultLanguageId_returnsNull() {
+ val profile =
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageId = LanguageId.getDefaultInstance())
+
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromMacaronic_languageIdWithoutMacaronic_returnsNull() {
+ val languageWithoutMacaronic = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageWithoutMacaronic)
+
+ // The provided language ID must have a macaronic ID defined.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromMacaronic_languageIdWithEmptyMacaronic_returnsNull() {
+ val languageWithMacaronic = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = ""
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+
+ // The provided language ID must have a macaronic ID defined.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromMacaronic_languageIdWithMalformedMacaronic_extraFields__returnsNull() {
+ val languageWithMacaronic = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "mal-form-ed"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+
+ // The provided language ID must have a well-formed macaronic ID defined.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromMacaronic_languageId_malformedMacaronic_missingSecondLang_returnsNull() {
+ val languageWithMacaronic = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+
+ // The provided language ID must have a well-formed macaronic ID defined, that is, it must have
+ // two language parts defined.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromMacaronic_languageId_emptyMacaronicRegion_returnsNull() {
+ val languageWithMacaronic = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+
+ // The macaronic ID has two parts as expected, but the second language ID must be filled in.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateProfileFromMacaronic_languageIdWithValidMacaronic_returnsProfile() {
+ val languageWithMacaronic = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-en"
+ }.build()
+ }.build()
+
+ val profile =
+ AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+
+ // The macaronic ID was valid. Verify that both language IDs correctly populate the profile.
+ assertThat(profile?.languageCode).isEqualTo("hi")
+ assertThat(profile?.regionCode).isEqualTo("en")
+ }
+
+ /* Tests for matches() */
+
+ @Test
+ fun testMatchProfile_rootProfile_withItself_match() {
+ val profile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+
+ val matches = profile.matches(machineLocale, profile)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_englishProfile_withItself_match() {
+ val profile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+
+ val matches = profile.matches(machineLocale, profile)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_brazilianPortuguese_withItself_match() {
+ val profile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ val matches = profile.matches(machineLocale, profile)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_englishProfile_withItselfInDifferentCase_match() {
+ val englishProfileLowercase = AndroidLocaleProfile(languageCode = "en", regionCode = "")
+ val englishProfileUppercase = AndroidLocaleProfile(languageCode = "EN", regionCode = "")
+
+ val matches = englishProfileLowercase.matches(machineLocale, englishProfileUppercase)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_englishProfile_withItselfInDifferentCase_reversed_match() {
+ val englishProfileLowercase = AndroidLocaleProfile(languageCode = "en", regionCode = "")
+ val englishProfileUppercase = AndroidLocaleProfile(languageCode = "EN", regionCode = "")
+
+ val matches = englishProfileUppercase.matches(machineLocale, englishProfileLowercase)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_brazilianPortuguese_withItselfInDifferentCase_match() {
+ val brazilianPortugueseProfileLowercase =
+ AndroidLocaleProfile(languageCode = "pt", regionCode = "br")
+ val brazilianPortugueseProfileUppercase =
+ AndroidLocaleProfile(languageCode = "PT", regionCode = "BR")
+
+ val matches =
+ brazilianPortugueseProfileLowercase.matches(
+ machineLocale, brazilianPortugueseProfileUppercase
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ fun testMatchProfile_brazilianPortuguese_withItselfInDifferentCase_reversed_match() {
+ val brazilianPortugueseProfileLowercase =
+ AndroidLocaleProfile(languageCode = "pt", regionCode = "br")
+ val brazilianPortugueseProfileUppercase =
+ AndroidLocaleProfile(languageCode = "PT", regionCode = "BR")
+
+ val matches =
+ brazilianPortugueseProfileUppercase.matches(
+ machineLocale, brazilianPortugueseProfileLowercase
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_rootProfile_english_doNotMatch() {
+ val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+
+ val matches = rootProfile.matches(machineLocale, englishProfile)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_rootProfile_brazilianPortuguese_doNotMatch() {
+ val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ val matches = rootProfile.matches(machineLocale, brazilianPortugueseProfile)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_english_brazilianPortuguese_doNotMatch() {
+ val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ val matches = englishProfile.matches(machineLocale, brazilianPortugueseProfile)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_rootProfile_englishWithWildcard_doNotMatch() {
+ val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ val englishWithWildcardProfile = createProfileWithWildcard(languageCode = "en")
+
+ val matches = rootProfile.matches(machineLocale, englishWithWildcardProfile)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_rootProfile_rootProfileWithWildcard_match() {
+ val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+
+ val matches = rootProfile.matches(machineLocale, rootProfileWithWildcardProfile)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_rootProfileWithWildcard_rootProfile_match() {
+ val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+
+ val matches = rootProfileWithWildcardProfile.matches(machineLocale, rootProfile)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_englishProfile_rootProfileWithWildcard_doNotMatch() {
+ val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+
+ val matches = englishProfile.matches(machineLocale, rootProfileWithWildcardProfile)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_englishWithWildcard_brazilianPortuguese_doNotMatch() {
+ val englishWithWildcardProfile = createProfileWithWildcard(languageCode = "en")
+ val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ val matches = englishWithWildcardProfile.matches(machineLocale, brazilianPortugueseProfile)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brazilianPortuguese_portuguese_doNotMatch() {
+ val portugueseProfile = AndroidLocaleProfile.createFrom(portugueseLocale)
+ val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ val matches = portugueseProfile.matches(machineLocale, brazilianPortugueseProfile)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brazilianPortuguese_portugueseWithWildcard_match() {
+ val portugueseWithWildcardProfile = createProfileWithWildcard(languageCode = "pt")
+ val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ val matches = brazilianPortugueseProfile.matches(machineLocale, portugueseWithWildcardProfile)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_portugueseWithWildcard_brazilianPortuguese_match() {
+ val portugueseWithWildcardProfile = createProfileWithWildcard(languageCode = "pt")
+ val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+
+ val matches = portugueseWithWildcardProfile.matches(machineLocale, brazilianPortugueseProfile)
+
+ assertThat(matches).isTrue()
+ }
+
+ private fun createProfileWithWildcard(languageCode: String): AndroidLocaleProfile =
+ AndroidLocaleProfile(languageCode, regionCode = AndroidLocaleProfile.REGION_WILDCARD)
+
+ private fun setUpTestApplicationComponent() {
+ DaggerAndroidLocaleProfileTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(androidLocaleProfileTest: AndroidLocaleProfileTest)
+ }
+
+ private companion object {
+ private val REGION_INDIA = RegionSupportDefinition.newBuilder().apply {
+ region = OppiaRegion.INDIA
+ regionId = IetfBcp47RegionId.newBuilder().apply {
+ ietfRegionTag = "IN"
+ }.build()
+ }.build()
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel
new file mode 100644
index 00000000000..43c6ea60c0c
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel
@@ -0,0 +1,100 @@
+"""
+Tests for language & locale utilities.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "AndroidLocaleProfileTest",
+ srcs = ["AndroidLocaleProfileTest.kt"],
+ custom_package = "org.oppia.android.util.locale",
+ test_class = "org.oppia.android.util.locale.AndroidLocaleProfileTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale:android_locale_profile",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ ],
+)
+
+oppia_android_test(
+ name = "LocaleProdModuleTest",
+ srcs = ["LocaleProdModuleTest.kt"],
+ custom_package = "org.oppia.android.util.locale",
+ test_class = "org.oppia.android.util.locale.LocaleProdModuleTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ ],
+)
+
+oppia_android_test(
+ name = "MachineLocaleImplTest",
+ srcs = ["MachineLocaleImplTest.kt"],
+ custom_package = "org.oppia.android.util.locale",
+ test_class = "org.oppia.android.util.locale.MachineLocaleImplTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ ],
+)
+
+oppia_android_test(
+ name = "OppiaBidiFormatterImplTest",
+ srcs = ["OppiaBidiFormatterImplTest.kt"],
+ custom_package = "org.oppia.android.util.locale",
+ test_class = "org.oppia.android.util.locale.OppiaBidiFormatterImplTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:shadow_bidi_formatter",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_bidi_formatter",
+ "//utility/src/main/java/org/oppia/android/util/locale:prod_module",
+ ],
+)
+
+oppia_android_test(
+ name = "OppiaLocaleContextExtensionsTest",
+ srcs = ["OppiaLocaleContextExtensionsTest.kt"],
+ custom_package = "org.oppia.android.util.locale",
+ test_class = "org.oppia.android.util.locale.OppiaLocaleContextExtensionsTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//model:languages_java_proto_lite",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_extensions_truth-liteproto-extension",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale",
+ ],
+)
+
+dagger_rules()
diff --git a/utility/src/test/java/org/oppia/android/util/locale/LocaleProdModuleTest.kt b/utility/src/test/java/org/oppia/android/util/locale/LocaleProdModuleTest.kt
new file mode 100644
index 00000000000..33cb92ef7cb
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/LocaleProdModuleTest.kt
@@ -0,0 +1,84 @@
+package org.oppia.android.util.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [LocaleProdModule]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class LocaleProdModuleTest {
+ @Inject
+ lateinit var machineLocale: OppiaLocale.MachineLocale
+
+ @Inject
+ lateinit var formatterFactory: OppiaBidiFormatter.Factory
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testModule_injectsProductionImplementationOfMachineLocale() {
+ assertThat(machineLocale).isInstanceOf(MachineLocaleImpl::class.java)
+ }
+
+ @Test
+ fun testModule_injectsProductionImplementationOfBidiFormatterFactory() {
+ assertThat(formatterFactory).isInstanceOf(OppiaBidiFormatterImpl.FactoryImpl::class.java)
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerLocaleProdModuleTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(localeProdModuleTest: LocaleProdModuleTest)
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/locale/MachineLocaleImplTest.kt b/utility/src/test/java/org/oppia/android/util/locale/MachineLocaleImplTest.kt
new file mode 100644
index 00000000000..888aa2cdc1c
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/MachineLocaleImplTest.kt
@@ -0,0 +1,308 @@
+package org.oppia.android.util.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.testing.time.FakeOppiaClock
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [MachineLocaleImpl]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class MachineLocaleImplTest {
+ @Inject
+ lateinit var machineLocale: OppiaLocale.MachineLocale
+
+ @Inject
+ lateinit var fakeOppiaClock: FakeOppiaClock
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
+ }
+
+ @Test
+ fun testFormatForMachines_formatStringWithArgs_returnsCorrectlyFormattedString() {
+ val formatted = machineLocale.run { "Test with %s and %d".formatForMachines("string", 11) }
+
+ assertThat(formatted).isEqualTo("Test with string and 11")
+ }
+
+ @Test
+ fun testToMachineLowerCase_lowerCaseString_returnsSameString() {
+ val formatted = machineLocale.run { "lowercase string".toMachineLowerCase() }
+
+ assertThat(formatted).isEqualTo("lowercase string")
+ }
+
+ @Test
+ fun testToMachineLowerCase_mixedCaseString_returnsLowerCaseString() {
+ val formatted = machineLocale.run { "MiXeD CaSe StriNG".toMachineLowerCase() }
+
+ assertThat(formatted).isEqualTo("mixed case string")
+ }
+
+ @Test
+ fun testToMachineUpperCase_upperCaseString_returnsSameString() {
+ val formatted = machineLocale.run { "UPPERCASE STRING".toMachineUpperCase() }
+
+ assertThat(formatted).isEqualTo("UPPERCASE STRING")
+ }
+
+ @Test
+ fun testToMachineUpperCase_mixedCaseString_returnsUpperCaseString() {
+ val formatted = machineLocale.run { "MiXeD CaSe StriNG".toMachineUpperCase() }
+
+ assertThat(formatted).isEqualTo("MIXED CASE STRING")
+ }
+
+ @Test
+ fun testToMachineLowerCase_englishLocale_localeSensitiveChar_returnsConvertedCase() {
+ Locale.setDefault(Locale.ENGLISH)
+
+ val formatted = machineLocale.run { "TITLE".toMachineLowerCase() }
+
+ assertThat(formatted).isEqualTo("title")
+ }
+
+ @Test
+ fun testToMachineLowerCase_turkishLocale_localeSensitiveChar_returnsSameConversion() {
+ Locale.setDefault(Locale("tr", "tr"))
+
+ val formatted = machineLocale.run { "TITLE".toMachineLowerCase() }
+
+ // See https://stackoverflow.com/a/11063161 for context on why this is correct behavior. The
+ // machine locale guarantees a consistent lowercase experience regardless of the current locale
+ // default (as comparable with the display locale which will perform case changing based on the
+ // locale).
+ assertThat(formatted).isEqualTo("title")
+ }
+
+ @Test
+ fun testCapitalizeForMachines_capitalizedString_returnsSameString() {
+ val formatted = machineLocale.run { "Capital".capitalizeForMachines() }
+
+ assertThat(formatted).isEqualTo("Capital")
+ }
+
+ @Test
+ fun testCapitalizeForMachines_uncapitalizedString_returnsCapitalized() {
+ val formatted = machineLocale.run { "uncapital".capitalizeForMachines() }
+
+ assertThat(formatted).isEqualTo("Uncapital")
+ }
+
+ @Test
+ fun testDecapitalizeForMachines_decapitalizedString_returnsSameString() {
+ val formatted = machineLocale.run { "uncapital".decapitalizeForMachines() }
+
+ assertThat(formatted).isEqualTo("uncapital")
+ }
+
+ @Test
+ fun testDecapitalizeForMachines_capitalizedString_returnsDecapitalized() {
+ val formatted = machineLocale.run { "Capital".decapitalizeForMachines() }
+
+ assertThat(formatted).isEqualTo("capital")
+ }
+
+ @Test
+ fun testEndsWithIgnoreCase_stringWithoutSuffix_returnsFalse() {
+ val matches = machineLocale.run { "string".endsWithIgnoreCase("aw") }
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testEndsWithIgnoreCase_stringWithSuffix_matchingCases_returnsTrue() {
+ val matches = machineLocale.run { "straw".endsWithIgnoreCase("aw") }
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testEndsWithIgnoreCase_stringWithSuffix_differingCases_returnsTrue() {
+ val matches = machineLocale.run { "STrAw".endsWithIgnoreCase("aW") }
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testEqualsIgnoreCase_differentString_returnsFalse() {
+ val matches = machineLocale.run { "string".equalsIgnoreCase("other") }
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testEqualsIgnoreCase_differentString_reversed_returnsFalse() {
+ val matches = machineLocale.run { "other".equalsIgnoreCase("string") }
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testEqualsIgnoreCase_sameString_matchingCases_returnsTrue() {
+ val matches = machineLocale.run { "string".equalsIgnoreCase("string") }
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testEqualsIgnoreCase_sameString_differingCases_returnsTrue() {
+ val matches = machineLocale.run { "sTRInG".equalsIgnoreCase("StrINg") }
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testEqualsIgnoreCase_sameString_differingCases_reversed_returnsTrue() {
+ val matches = machineLocale.run { "StrINg".equalsIgnoreCase("sTRInG") }
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testGetCurrentTimeOfDay_inMorningHour_returnsMorning() {
+ fakeOppiaClock.setCurrentTimeMs(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ val timeOfDay = machineLocale.getCurrentTimeOfDay()
+
+ assertThat(timeOfDay).isEqualTo(OppiaLocale.MachineLocale.TimeOfDay.MORNING)
+ }
+
+ @Test
+ fun testGetCurrentTimeOfDay_inAfternoonHour_returnsAfternoon() {
+ fakeOppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS)
+
+ val timeOfDay = machineLocale.getCurrentTimeOfDay()
+
+ assertThat(timeOfDay).isEqualTo(OppiaLocale.MachineLocale.TimeOfDay.AFTERNOON)
+ }
+
+ @Test
+ fun testGetCurrentTimeOfDay_inEveningHour_returnsEvening() {
+ fakeOppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS)
+
+ val timeOfDay = machineLocale.getCurrentTimeOfDay()
+
+ assertThat(timeOfDay).isEqualTo(OppiaLocale.MachineLocale.TimeOfDay.EVENING)
+ }
+
+ @Test
+ fun testParseOppiaDate_emptyString_returnsNull() {
+ fakeOppiaClock.setCurrentTimeMs(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ val date = machineLocale.parseOppiaDate(dateString = "")
+
+ assertThat(date).isNull()
+ }
+
+ @Test
+ fun testParseOppiaDate_invalidDateFormat_returnsNull() {
+ val date = machineLocale.parseOppiaDate(dateString = "24 April 2019")
+
+ // The date is in an invalid/unexpected format.
+ assertThat(date).isNull()
+ }
+
+ @Test
+ fun testParseOppiaDate_validDateFormat_returnsDateObject() {
+ val date = machineLocale.parseOppiaDate(dateString = "2019-04-24")
+
+ assertThat(date).isNotNull()
+ }
+
+ @Test
+ fun testOppiaDateIsBeforeToday_validDateObject_forYesterday_returnsTrue() {
+ fakeOppiaClock.setCurrentTimeMs(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ val date = machineLocale.parseOppiaDate(dateString = "2019-04-23")
+
+ assertThat(date?.isBeforeToday()).isTrue()
+ }
+
+ @Test
+ fun testOppiaDateIsBeforeToday_validDateObject_forTomorrow_returnsFalse() {
+ fakeOppiaClock.setCurrentTimeMs(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ val date = machineLocale.parseOppiaDate(dateString = "2019-04-25")
+
+ assertThat(date?.isBeforeToday()).isFalse()
+ }
+
+ @Test
+ fun testComputeCurrentTimeString_forFixedTime_returnsHourMinuteSecondParts() {
+ fakeOppiaClock.setCurrentTimeMs(MORNING_UTC_TIMESTAMP_MILLIS)
+
+ val timeString = machineLocale.computeCurrentTimeString()
+
+ // Verify that the individual hour, minute, and second components are present (though don't
+ // actually verify formatting since that's implementation specific).
+ val numbers = "\\d+".toRegex().findAll(timeString).flatMap { it.groupValues }.toList()
+ assertThat(numbers).containsExactly("8", "22", "03")
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerMachineLocaleImplTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(machineLocaleImplTest: MachineLocaleImplTest)
+ }
+
+ private companion object {
+ // Date & time: Wed Apr 24 2019 08:22:03 GMT.
+ private const val MORNING_UTC_TIMESTAMP_MILLIS = 1556094123000
+ // Date & time: Tue Apr 23 2019 14:22:00 GMT.
+ private const val AFTERNOON_UTC_TIMESTAMP_MILLIS = 1556029320000
+ // Date & time: Tue Apr 23 2019 23:22:00 GMT.
+ private const val EVENING_UTC_TIMESTAMP_MILLIS = 1556061720000
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/locale/OppiaBidiFormatterImplTest.kt b/utility/src/test/java/org/oppia/android/util/locale/OppiaBidiFormatterImplTest.kt
new file mode 100644
index 00000000000..d8b5c05dbf5
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/OppiaBidiFormatterImplTest.kt
@@ -0,0 +1,162 @@
+package org.oppia.android.util.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.testing.robolectric.ShadowBidiFormatter
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [OppiaBidiFormatterImpl]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE, shadows = [ShadowBidiFormatter::class])
+class OppiaBidiFormatterImplTest {
+ @Inject
+ lateinit var formatterFactory: OppiaBidiFormatter.Factory
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @After
+ fun tearDown() {
+ // Make sure this is reset between tests.
+ ShadowBidiFormatter.reset()
+ }
+
+ @Test
+ fun testInjectFactory_noFormattersCreator_hasZeroTrackedFormatters() {
+ assertThat(ShadowBidiFormatter.lookUpFormatters()).isEmpty()
+ assertThat(ShadowBidiFormatter.lookUpFormatter(Locale.US)).isNull()
+ }
+
+ @Test
+ fun testFactory_rootLocale_doNothingWithFormatter_doesNotCreateNewBidiFormatter() {
+ formatterFactory.createFormatter(Locale.ROOT)
+
+ // The wrapper only creates the formatter when it's needed.
+ assertThat(ShadowBidiFormatter.lookUpFormatter(Locale.ROOT)).isNull()
+ }
+
+ @Test
+ fun testFactory_rootLocale_createsNewFormatterForRootLocale() {
+ // Some text needs to be wrapped to guarantee the formatter was created.
+ formatterFactory.createFormatter(Locale.ROOT).wrapText("")
+
+ assertThat(ShadowBidiFormatter.lookUpFormatter(Locale.ROOT)).isNotNull()
+ }
+
+ @Test
+ fun testFactory_usLocale_createsNewFormatterForUsLocale() {
+ // Some text needs to be wrapped to guarantee the formatter was created.
+ formatterFactory.createFormatter(Locale.US).wrapText("")
+
+ assertThat(ShadowBidiFormatter.lookUpFormatter(Locale.US)).isNotNull()
+ }
+
+ @Test
+ fun testFactory_multipleLocales_createsFormatterForEach() {
+ formatterFactory.createFormatter(INDIA_HINDI_LOCALE).wrapText("")
+ formatterFactory.createFormatter(BRAZIL_PORTUGUESE_LOCALE).wrapText("")
+ formatterFactory.createFormatter(Locale.US).wrapText("")
+
+ // Note that this more verifies the shadow since Android caches the actual bidi formatter
+ // instance internally.
+ assertThat(ShadowBidiFormatter.lookUpFormatter(INDIA_HINDI_LOCALE)).isNotNull()
+ assertThat(ShadowBidiFormatter.lookUpFormatter(BRAZIL_PORTUGUESE_LOCALE)).isNotNull()
+ assertThat(ShadowBidiFormatter.lookUpFormatter(Locale.US)).isNotNull()
+ }
+
+ @Test
+ fun testFormatter_wrapString_indiaLocale_callsFormatterUnicodeWrap() {
+ val formatter = formatterFactory.createFormatter(INDIA_HINDI_LOCALE)
+
+ formatter.wrapText("test str")
+
+ val shadow = ShadowBidiFormatter.lookUpFormatter(INDIA_HINDI_LOCALE)
+ assertThat(shadow?.getLastWrappedSequence()).isEqualTo("test str")
+ }
+
+ @Test
+ fun testFormatter_wrapString_rtlLocale_doesNotCallFormatterUnicodeWrapForLtrLocale() {
+ val ltrFormatter = formatterFactory.createFormatter(INDIA_HINDI_LOCALE)
+ val rtlFormatter = formatterFactory.createFormatter(EGYPT_ARABIC_LOCALE)
+
+ ltrFormatter.wrapText("test LTR string")
+ rtlFormatter.wrapText("test RTL string (sort of)")
+
+ val shadow = ShadowBidiFormatter.lookUpFormatter(INDIA_HINDI_LOCALE)
+ assertThat(shadow?.getLastWrappedSequence()).isEqualTo("test LTR string")
+ }
+
+ @Test
+ fun testFormatter_wrapMultipleString_brazilLocale_callsUnicodeWrapForEach() {
+ val formatter = formatterFactory.createFormatter(INDIA_HINDI_LOCALE)
+
+ formatter.wrapText("test str one")
+ formatter.wrapText("test str two")
+
+ val shadow = ShadowBidiFormatter.lookUpFormatter(INDIA_HINDI_LOCALE)
+ assertThat(shadow?.getAllWrappedSequences())
+ .containsExactly("test str one", "test str two")
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerOppiaBidiFormatterImplTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleProdModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(oppiaBidiFormatterImplTest: OppiaBidiFormatterImplTest)
+ }
+
+ private companion object {
+ private val INDIA_HINDI_LOCALE = Locale("hi", "IN")
+ private val BRAZIL_PORTUGUESE_LOCALE = Locale("pt", "BR")
+ private val EGYPT_ARABIC_LOCALE = Locale("ar", "EG")
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/locale/OppiaLocaleContextExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/locale/OppiaLocaleContextExtensionsTest.kt
new file mode 100644
index 00000000000..c14734a7e43
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/OppiaLocaleContextExtensionsTest.kt
@@ -0,0 +1,650 @@
+package org.oppia.android.util.locale
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.model.LanguageSupportDefinition
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
+import org.oppia.android.app.model.OppiaLanguage
+import org.oppia.android.app.model.OppiaLocaleContext
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Singleton
+
+/** Tests for [OppiaLocaleContext] extensions. */
+// FunctionName: test names are conventionally named with underscores.
+// SameParameterValue: tests should have specific context included/excluded for readability.
+@Suppress("FunctionName", "SameParameterValue")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class OppiaLocaleContextExtensionsTest {
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ /* Tests for getLanguageId() */
+
+ @Test
+ fun testGetLanguageId_defaultInstance_returnsDefaultInstance() {
+ val localeContext = OppiaLocaleContext.getDefaultInstance()
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetLanguageId_appStringUsage_noAppStringId_returnsDefaultInstance() {
+ val localeContext = createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetLanguageId_appStringUsage_definedAppStringId_returnsAppStringId() {
+ val localeContext = createAppStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_contentStringUsage_noContentStringId_returnsDefaultInstance() {
+ val localeContext = createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetLanguageId_contentStringUsage_definedContentStringId_returnsContentId() {
+ val localeContext = createContentStringsContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = ENGLISH_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_audioSubsUsage_noAudioSubId_returnsDefaultInstance() {
+ val localeContext = createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetLanguageId_audioSubsUsage_definedAudioSubId_returnsAudioSubId() {
+ val localeContext = createAudioSubContext(
+ language = OppiaLanguage.ENGLISH,
+ audioTranslationId = ENGLISH_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_fullDefinition_appStringUsage_returnsAppStringId() {
+ val localeContext = createCompleteLocaleContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_fullDefinition_contentStringUsage_returnsContentStringId() {
+ val localeContext = createCompleteLocaleContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualTo(HINGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_fullDefinition_audioSubUsage_returnsAudioSubId() {
+ val localeContext = createCompleteLocaleContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualTo(BRAZILIAN_PORTUGUESE_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_fullDefinition_unspecifiedUsage_returnsDefaultInstance() {
+ val localeContext = createCompleteLocaleContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.USAGE_MODE_UNSPECIFIED,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ assertThat(languageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetLanguageId_fullDefinitionWithFallback_appStringUsage_returnsAppStringId() {
+ val localeContext = createCompleteLocaleWithFallbackContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID,
+ fallbackAppStringId = QQ_LANGUAGE_ID,
+ fallbackContentStringId = ZZ_LANGUAGE_ID,
+ fallbackAudioTranslationId = FAKE_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ // The fallback language should be ignored.
+ assertThat(languageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_fullDefinitionWithFallback_contentStringUsage_returnsContentStringId() {
+ val localeContext = createCompleteLocaleWithFallbackContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID,
+ fallbackAppStringId = QQ_LANGUAGE_ID,
+ fallbackContentStringId = ZZ_LANGUAGE_ID,
+ fallbackAudioTranslationId = FAKE_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ // The fallback language should be ignored.
+ assertThat(languageId).isEqualTo(HINGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetLanguageId_fullDefinitionWithFallback_audioSubUsage_returnsAudioSubId() {
+ val localeContext = createCompleteLocaleWithFallbackContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID,
+ fallbackAppStringId = QQ_LANGUAGE_ID,
+ fallbackContentStringId = ZZ_LANGUAGE_ID,
+ fallbackAudioTranslationId = FAKE_LANGUAGE_ID
+ )
+
+ val languageId = localeContext.getLanguageId()
+
+ // The fallback language should be ignored.
+ assertThat(languageId).isEqualTo(BRAZILIAN_PORTUGUESE_LANGUAGE_ID)
+ }
+
+ /* Tests for getFallbackLanguageId() */
+
+ @Test
+ fun testGetFallbackId_defaultInstance_returnsDefaultInstance() {
+ val localeContext = OppiaLocaleContext.getDefaultInstance()
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetFallbackId_appStringUsage_noAppStringId_returnsDefaultInstance() {
+ val localeContext = createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetFallbackId_appStringUsage_definedFallbackAppStringId_returnsAppStringId() {
+ val localeContext = createAppStringsWithFallbackContext(
+ language = OppiaLanguage.ENGLISH,
+ appStringId = QQ_LANGUAGE_ID,
+ fallbackStringId = ENGLISH_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // The fallback ID should be selected.
+ assertThat(fallbackLanguageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetFallbackId_contentStringUsage_noContentStringId_returnsDefaultInstance() {
+ val localeContext = createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetFallbackId_contentStringUsage_definedFallbackContentStringId_returnsContentId() {
+ val localeContext = createContentStringsWithFallbackContext(
+ language = OppiaLanguage.ENGLISH,
+ contentStringId = QQ_LANGUAGE_ID,
+ fallbackContentStringId = ENGLISH_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // The fallback ID should be selected.
+ assertThat(fallbackLanguageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetFallbackId_audioSubsUsage_noAudioSubId_returnsDefaultInstance() {
+ val localeContext = createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetFallbackId_audioSubsUsage_definedFallbackAudioSubId_returnsAudioSubId() {
+ val localeContext = createAudioSubWithFallbackContext(
+ language = OppiaLanguage.ENGLISH,
+ audioTranslationId = QQ_LANGUAGE_ID,
+ fallbackAudioTranslationId = ENGLISH_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // The fallback ID should be selected.
+ assertThat(fallbackLanguageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetFallbackId_fullDefinition_appStringUsage_returnsAppStringId() {
+ val localeContext = createCompleteLocaleWithFallbackContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = QQ_LANGUAGE_ID,
+ contentStringId = ZZ_LANGUAGE_ID,
+ audioTranslationId = FAKE_LANGUAGE_ID,
+ fallbackAppStringId = ENGLISH_LANGUAGE_ID,
+ fallbackContentStringId = HINGLISH_LANGUAGE_ID,
+ fallbackAudioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // The fallback ID should match the specified usage mode.
+ assertThat(fallbackLanguageId).isEqualTo(ENGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetFallbackId_fullDefinition_contentStringUsage_returnsContentStringId() {
+ val localeContext = createCompleteLocaleWithFallbackContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = QQ_LANGUAGE_ID,
+ contentStringId = ZZ_LANGUAGE_ID,
+ audioTranslationId = FAKE_LANGUAGE_ID,
+ fallbackAppStringId = ENGLISH_LANGUAGE_ID,
+ fallbackContentStringId = HINGLISH_LANGUAGE_ID,
+ fallbackAudioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // The fallback ID should match the specified usage mode.
+ assertThat(fallbackLanguageId).isEqualTo(HINGLISH_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetFallbackId_fullDefinition_audioSubUsage_returnsAudioSubId() {
+ val localeContext = createCompleteLocaleWithFallbackContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = QQ_LANGUAGE_ID,
+ contentStringId = ZZ_LANGUAGE_ID,
+ audioTranslationId = FAKE_LANGUAGE_ID,
+ fallbackAppStringId = ENGLISH_LANGUAGE_ID,
+ fallbackContentStringId = HINGLISH_LANGUAGE_ID,
+ fallbackAudioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // The fallback ID should match the specified usage mode.
+ assertThat(fallbackLanguageId).isEqualTo(BRAZILIAN_PORTUGUESE_LANGUAGE_ID)
+ }
+
+ @Test
+ fun testGetFallbackId_fullDefinition_unspecifiedUsage_returnsDefaultInstance() {
+ val localeContext = createCompleteLocaleWithFallbackContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.USAGE_MODE_UNSPECIFIED,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = QQ_LANGUAGE_ID,
+ contentStringId = ZZ_LANGUAGE_ID,
+ audioTranslationId = FAKE_LANGUAGE_ID,
+ fallbackAppStringId = ENGLISH_LANGUAGE_ID,
+ fallbackContentStringId = HINGLISH_LANGUAGE_ID,
+ fallbackAudioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetFallbackId_fullDefinitionNoFallback_appStringUsage_returnsDefaultInstance() {
+ val localeContext = createCompleteLocaleContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // No fallback is defined.
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetFallbackId_fullDefinitionNoFallback_contentStringUsage_returnsDefaultInstance() {
+ val localeContext = createCompleteLocaleContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // No fallback is defined.
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ @Test
+ fun testGetFallbackId_fullDefinitionNoFallback_audioSubUsage_returnsDefaultInstance() {
+ val localeContext = createCompleteLocaleContext(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS,
+ language = OppiaLanguage.ENGLISH,
+ appStringId = ENGLISH_LANGUAGE_ID,
+ contentStringId = HINGLISH_LANGUAGE_ID,
+ audioTranslationId = BRAZILIAN_PORTUGUESE_LANGUAGE_ID
+ )
+
+ val fallbackLanguageId = localeContext.getFallbackLanguageId()
+
+ // No fallback is defined.
+ assertThat(fallbackLanguageId).isEqualToDefaultInstance()
+ }
+
+ private fun createContextWithoutLanguageDefinition(
+ usageMode: OppiaLocaleContext.LanguageUsageMode
+ ): OppiaLocaleContext {
+ return OppiaLocaleContext.newBuilder().apply {
+ this.usageMode = usageMode
+ }.build()
+ }
+
+ private fun createAppStringsContext(
+ language: OppiaLanguage,
+ appStringId: LanguageId
+ ): OppiaLocaleContext {
+ return createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.APP_STRINGS
+ ).toBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.appStringId = appStringId
+ }.build()
+ }.build()
+ }
+
+ private fun createAppStringsWithFallbackContext(
+ language: OppiaLanguage,
+ appStringId: LanguageId,
+ fallbackStringId: LanguageId
+ ): OppiaLocaleContext {
+ return createAppStringsContext(language, appStringId).toBuilder().apply {
+ fallbackLanguageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.appStringId = fallbackStringId
+ }.build()
+ }.build()
+ }
+
+ private fun createContentStringsContext(
+ language: OppiaLanguage,
+ contentStringId: LanguageId
+ ): OppiaLocaleContext {
+ return createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.CONTENT_STRINGS
+ ).toBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.contentStringId = contentStringId
+ }.build()
+ }.build()
+ }
+
+ private fun createContentStringsWithFallbackContext(
+ language: OppiaLanguage,
+ contentStringId: LanguageId,
+ fallbackContentStringId: LanguageId
+ ): OppiaLocaleContext {
+ return createContentStringsContext(language, contentStringId).toBuilder().apply {
+ fallbackLanguageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.contentStringId = fallbackContentStringId
+ }.build()
+ }.build()
+ }
+
+ private fun createAudioSubContext(
+ language: OppiaLanguage,
+ audioTranslationId: LanguageId
+ ): OppiaLocaleContext {
+ return createContextWithoutLanguageDefinition(
+ usageMode = OppiaLocaleContext.LanguageUsageMode.AUDIO_TRANSLATIONS
+ ).toBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.audioTranslationId = audioTranslationId
+ }.build()
+ }.build()
+ }
+
+ private fun createAudioSubWithFallbackContext(
+ language: OppiaLanguage,
+ audioTranslationId: LanguageId,
+ fallbackAudioTranslationId: LanguageId
+ ): OppiaLocaleContext {
+ return createAudioSubContext(language, audioTranslationId).toBuilder().apply {
+ fallbackLanguageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.audioTranslationId = fallbackAudioTranslationId
+ }.build()
+ }.build()
+ }
+
+ private fun createCompleteLocaleContext(
+ usageMode: OppiaLocaleContext.LanguageUsageMode,
+ language: OppiaLanguage,
+ appStringId: LanguageId,
+ contentStringId: LanguageId,
+ audioTranslationId: LanguageId
+ ): OppiaLocaleContext {
+ return createContextWithoutLanguageDefinition(usageMode).toBuilder().apply {
+ languageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.appStringId = appStringId
+ this.contentStringId = contentStringId
+ this.audioTranslationId = audioTranslationId
+ }.build()
+ }.build()
+ }
+
+ private fun createCompleteLocaleWithFallbackContext(
+ usageMode: OppiaLocaleContext.LanguageUsageMode,
+ language: OppiaLanguage,
+ appStringId: LanguageId,
+ contentStringId: LanguageId,
+ audioTranslationId: LanguageId,
+ fallbackAppStringId: LanguageId,
+ fallbackContentStringId: LanguageId,
+ fallbackAudioTranslationId: LanguageId
+ ): OppiaLocaleContext {
+ return createCompleteLocaleContext(
+ usageMode, language, appStringId, contentStringId, audioTranslationId
+ ).toBuilder().apply {
+ fallbackLanguageDefinition = LanguageSupportDefinition.newBuilder().apply {
+ this.language = language
+ this.appStringId = fallbackAppStringId
+ this.contentStringId = fallbackContentStringId
+ this.audioTranslationId = fallbackAudioTranslationId
+ }.build()
+ }.build()
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerOppiaLocaleContextExtensionsTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(oppiaLocaleContextExtensionsTest: OppiaLocaleContextExtensionsTest)
+ }
+
+ private companion object {
+ private val ENGLISH_LANGUAGE_ID = LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "en"
+ }.build()
+ androidResourcesLanguageId = LanguageSupportDefinition.AndroidLanguageId.newBuilder().apply {
+ languageCode = "en"
+ }.build()
+ }.build()
+
+ private val HINGLISH_LANGUAGE_ID = LanguageId.newBuilder().apply {
+ macaronicId = LanguageSupportDefinition.MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-en"
+ }.build()
+ }.build()
+
+ private val BRAZILIAN_PORTUGUESE_LANGUAGE_ID = LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt-BR"
+ }.build()
+ androidResourcesLanguageId = LanguageSupportDefinition.AndroidLanguageId.newBuilder().apply {
+ languageCode = "pt"
+ regionCode = "BR"
+ }.build()
+ }.build()
+
+ private val QQ_LANGUAGE_ID = LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "qq"
+ }.build()
+ }.build()
+
+ private val ZZ_LANGUAGE_ID = LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "zz"
+ }.build()
+ }.build()
+
+ private val FAKE_LANGUAGE_ID = LanguageId.newBuilder().apply {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "fake"
+ }.build()
+ }.build()
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/locale/testing/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/locale/testing/BUILD.bazel
new file mode 100644
index 00000000000..e8457492b52
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/testing/BUILD.bazel
@@ -0,0 +1,46 @@
+"""
+Tests for testing utilities for language & locale general utilities.
+"""
+
+load("@dagger//:workspace_defs.bzl", "dagger_rules")
+load("//:oppia_android_test.bzl", "oppia_android_test")
+
+oppia_android_test(
+ name = "LocaleTestModuleTest",
+ srcs = ["LocaleTestModuleTest.kt"],
+ custom_package = "org.oppia.android.util.locale.testing",
+ test_class = "org.oppia.android.util.locale.testing.LocaleTestModuleTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing/src/main/java/org/oppia/android/testing/time:test_module",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale/testing:test_module",
+ ],
+)
+
+oppia_android_test(
+ name = "TestOppiaBidiFormatterTest",
+ srcs = ["TestOppiaBidiFormatterTest.kt"],
+ custom_package = "org.oppia.android.util.locale.testing",
+ test_class = "org.oppia.android.util.locale.testing.TestOppiaBidiFormatterTest",
+ test_manifest = "//utility:test_manifest",
+ deps = [
+ ":dagger",
+ "//testing",
+ "//testing/src/main/java/org/oppia/android/testing/robolectric:shadow_bidi_formatter",
+ "//third_party:androidx_test_ext_junit",
+ "//third_party:com_google_truth_truth",
+ "//third_party:junit_junit",
+ "//third_party:org_robolectric_robolectric",
+ "//third_party:robolectric_android-all",
+ "//utility/src/main/java/org/oppia/android/util/locale/testing:test_module",
+ "//utility/src/main/java/org/oppia/android/util/locale/testing:test_oppia_bidi_formatter",
+ ],
+)
+
+dagger_rules()
diff --git a/utility/src/test/java/org/oppia/android/util/locale/testing/LocaleTestModuleTest.kt b/utility/src/test/java/org/oppia/android/util/locale/testing/LocaleTestModuleTest.kt
new file mode 100644
index 00000000000..159b8cee2bb
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/testing/LocaleTestModuleTest.kt
@@ -0,0 +1,87 @@
+package org.oppia.android.util.locale.testing
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.locale.MachineLocaleImpl
+import org.oppia.android.util.locale.OppiaBidiFormatter
+import org.oppia.android.util.locale.OppiaLocale
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [LocaleTestModule]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class LocaleTestModuleTest {
+ @Inject
+ lateinit var machineLocale: OppiaLocale.MachineLocale
+
+ @Inject
+ lateinit var formatterFactory: OppiaBidiFormatter.Factory
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @Test
+ fun testModule_injectsProductionImplementationOfMachineLocale() {
+ assertThat(machineLocale).isInstanceOf(MachineLocaleImpl::class.java)
+ }
+
+ @Test
+ fun testModule_injectsTestImplementationOfBidiFormatterFactory() {
+ assertThat(formatterFactory).isInstanceOf(TestOppiaBidiFormatter.FactoryImpl::class.java)
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerLocaleTestModuleTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleTestModule::class, FakeOppiaClockModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(localeTestModuleTest: LocaleTestModuleTest)
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/locale/testing/TestOppiaBidiFormatterTest.kt b/utility/src/test/java/org/oppia/android/util/locale/testing/TestOppiaBidiFormatterTest.kt
new file mode 100644
index 00000000000..d56be1961ae
--- /dev/null
+++ b/utility/src/test/java/org/oppia/android/util/locale/testing/TestOppiaBidiFormatterTest.kt
@@ -0,0 +1,167 @@
+package org.oppia.android.util.locale.testing
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.testing.assertThrows
+import org.oppia.android.testing.robolectric.ShadowBidiFormatter
+import org.oppia.android.util.locale.OppiaBidiFormatter
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import java.util.Locale
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [TestOppiaBidiFormatter]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE, shadows = [ShadowBidiFormatter::class])
+class TestOppiaBidiFormatterTest {
+ @Inject
+ lateinit var formatterFactory: OppiaBidiFormatter.Factory
+
+ @Inject
+ lateinit var formatterChecker: TestOppiaBidiFormatter.Checker
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ }
+
+ @After
+ fun tearDown() {
+ ShadowBidiFormatter.reset()
+ }
+
+ @Test
+ fun testFactory_createTwoInstances_sameLocale_returnsDifferentFormatters() {
+ val formatter1 = formatterFactory.createFormatter(Locale.ROOT)
+ val formatter2 = formatterFactory.createFormatter(Locale.US)
+
+ assertThat(formatter1).isNotEqualTo(formatter2)
+ }
+
+ @Test
+ fun testFormatter_wrapText_callsProdWrapText() {
+ val formatter = formatterFactory.createFormatter(Locale.US)
+
+ formatter.wrapText("test str")
+
+ // Verify via the custom shadow that the production BidiFormatter is called (doesn't necessarily
+ // guarantee that this class calls through to the prod implementation, but from the API
+ // perspective of OppiaBidiFormatter this distinction is unimportant).
+ val shadow = ShadowBidiFormatter.lookUpFormatter(Locale.US)
+ assertThat(shadow?.getLastWrappedSequence()).isEqualTo("test str")
+ }
+
+ @Test
+ fun testFormatter_wrapText_returnsSequenceWithSameLength() {
+ val formatter = formatterFactory.createFormatter(Locale.US)
+ val strToWrap = "test str"
+
+ val wrappedStr = formatter.wrapText(strToWrap)
+
+ assertThat(wrappedStr.length).isEqualTo(wrappedStr.length)
+ }
+
+ @Test
+ fun testFormatter_wrapText_returnsSequenceWithSameContents() {
+ val formatter = formatterFactory.createFormatter(Locale.US)
+
+ val wrappedStr = formatter.wrapText("test str")
+
+ assertThat(wrappedStr).isEqualTo("test str")
+ assertThat(wrappedStr.toString()).isEqualTo("test str")
+ }
+
+ @Test
+ fun testFormatter_wrapText_twice_throwsException() {
+ val formatter = formatterFactory.createFormatter(Locale.US)
+ val wrappedStr = formatter.wrapText("test str")
+
+ // Try to wrap the string again.
+ val exception = assertThrows(IllegalStateException::class) { formatter.wrapText(wrappedStr) }
+
+ assertThat(exception).hasMessageThat()
+ .contains("Error: encountered string that's already been wrapped: test str")
+ }
+
+ @Test
+ fun testChecker_isTextWrapped_unwrappedText_returnsFalse() {
+ val isWrapped = formatterChecker.isTextWrapped("test str")
+
+ assertThat(isWrapped).isFalse()
+ }
+
+ @Test
+ fun testChecker_isTextWrapped_wrappedText_returnsTrue() {
+ val formatter = formatterFactory.createFormatter(Locale.US)
+ val wrapped = formatter.wrapText("test str")
+
+ val isWrapped = formatterChecker.isTextWrapped(wrapped)
+
+ assertThat(isWrapped).isTrue()
+ }
+
+ @Test
+ fun testChecker_isTextWrapped_wrappedText_fromDifferentFormatters_returnsTrue() {
+ val ltrFormatter = formatterFactory.createFormatter(Locale.US)
+ val rtlFormatter = formatterFactory.createFormatter(Locale("ar", "EG"))
+ val wrapped1 = ltrFormatter.wrapText("test str one")
+ val wrapped2 = rtlFormatter.wrapText("test str two")
+
+ val isFirstWrapped = formatterChecker.isTextWrapped(wrapped1)
+ val isSecondWrapped = formatterChecker.isTextWrapped(wrapped2)
+
+ assertThat(isFirstWrapped).isTrue()
+ assertThat(isSecondWrapped).isTrue()
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerTestOppiaBidiFormatterTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, LocaleTestModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(testOppiaBidiFormatterTest: TestOppiaBidiFormatterTest)
+ }
+}
diff --git a/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt b/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt
index 469e9073623..a9dd9d111af 100644
--- a/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt
@@ -20,6 +20,7 @@ import org.oppia.android.testing.TestImageLoaderModule
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
import org.oppia.android.util.logging.LoggerModule
import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever
@@ -135,7 +136,7 @@ class UrlImageParserTest {
modules = [
TestModule::class, TestDispatcherModule::class, RobolectricModule::class,
FakeOppiaClockModule::class, LoggerModule::class, TestImageLoaderModule::class,
- CachingTestModule::class, ImageParsingModule::class
+ CachingTestModule::class, ImageParsingModule::class, AssetModule::class
]
)
interface TestApplicationComponent {