diff --git a/app/BUILD.bazel b/app/BUILD.bazel
index 834ddb79025..2e155aeb696 100644
--- a/app/BUILD.bazel
+++ b/app/BUILD.bazel
@@ -222,6 +222,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt",
"src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt",
"src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt",
+ "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt",
@@ -321,7 +322,6 @@ VIEW_MODELS = [
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContentViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueNavigationButtonViewModel.kt",
- "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/FeedbackViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/NextButtonViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt",
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 24f808fc31f..1dee1b80db5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -215,6 +215,7 @@
+
@@ -224,6 +225,7 @@
+
diff --git a/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt b/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt
index 67fd30fdf06..39ae00f3628 100644
--- a/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt
+++ b/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt
@@ -76,6 +76,7 @@ import org.oppia.android.app.testing.ImageViewBindingAdaptersTestActivity
import org.oppia.android.app.testing.InputInteractionViewTestActivity
import org.oppia.android.app.testing.ListItemLeadingMarginSpanTestActivity
import org.oppia.android.app.testing.MarginBindingAdaptersTestActivity
+import org.oppia.android.app.testing.MathExpressionInteractionsViewTestActivity
import org.oppia.android.app.testing.NavigationDrawerTestActivity
import org.oppia.android.app.testing.PoliciesFragmentTestActivity
import org.oppia.android.app.testing.ProfileChooserFragmentTestActivity
@@ -86,6 +87,7 @@ import org.oppia.android.app.testing.SpotlightFragmentTestActivity
import org.oppia.android.app.testing.StateAssemblerMarginBindingAdaptersTestActivity
import org.oppia.android.app.testing.StateAssemblerPaddingBindingAdaptersTestActivity
import org.oppia.android.app.testing.TestFontScaleConfigurationUtilActivity
+import org.oppia.android.app.testing.TextInputInteractionViewTestActivity
import org.oppia.android.app.testing.TextViewBindingAdaptersTestActivity
import org.oppia.android.app.testing.TopicRevisionTestActivity
import org.oppia.android.app.testing.TopicTestActivity
@@ -154,6 +156,8 @@ interface ActivityComponentImpl :
fun inject(imageRegionSelectionTestActivity: ImageRegionSelectionTestActivity)
fun inject(imageViewBindingAdaptersTestActivity: ImageViewBindingAdaptersTestActivity)
fun inject(inputInteractionViewTestActivity: InputInteractionViewTestActivity)
+ fun inject(textInputInteractionViewTestActivity: TextInputInteractionViewTestActivity)
+ fun inject(mathExpressionInteractionsViewTestActivity: MathExpressionInteractionsViewTestActivity)
fun inject(ratioInputInteractionViewTestActivity: RatioInputInteractionViewTestActivity)
fun inject(licenseListActivity: LicenseListActivity)
fun inject(licenseTextViewerActivity: LicenseTextViewerActivity)
diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt
index 6cd22508141..d6f88ea9cec 100644
--- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt
@@ -1,8 +1,10 @@
package org.oppia.android.app.player.state.itemviewmodel
+import androidx.annotation.StringRes
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.recyclerview.widget.RecyclerView
+import org.oppia.android.R
import org.oppia.android.app.model.Interaction
import org.oppia.android.app.model.InteractionObject
import org.oppia.android.app.model.ListOfSetsOfHtmlStrings
@@ -13,6 +15,7 @@ import org.oppia.android.app.model.SubtitledHtml
import org.oppia.android.app.model.TranslatableHtmlContentId
import org.oppia.android.app.model.UserAnswer
import org.oppia.android.app.model.WrittenTranslationContext
+import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
@@ -23,6 +26,18 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.domain.translation.TranslationController
import javax.inject.Inject
+/** Represents the type of errors that can be thrown by drag and drop sort interaction. */
+enum class DragAndDropSortInteractionError(@StringRes private var error: Int?) {
+ VALID(error = null),
+ EMPTY_INPUT(error = R.string.drag_and_drop_interaction_empty_input);
+
+ /**
+ * Returns the string corresponding to this error's string resources, or null if there is none.
+ */
+ fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? =
+ error?.let(resourceHandler::getStringInLocale)
+}
+
/** [StateItemViewModel] for drag drop & sort choice list. */
class DragAndDropSortInteractionViewModel private constructor(
val entityId: String,
@@ -55,25 +70,34 @@ class DragAndDropSortInteractionViewModel private constructor(
subtitledHtml.contentId to translatedHtml
}
- private val _choiceItems: MutableList =
+ private val _originalChoiceItems: MutableList =
computeChoiceItems(contentIdHtmlMap, choiceSubtitledHtmls, this, resourceHandler)
+ private val _choiceItems = _originalChoiceItems.toMutableList()
val choiceItems: List = _choiceItems
+ private var pendingAnswerError: String? = null
private val isAnswerAvailable = ObservableField(false)
+ var errorMessage = ObservableField("")
init {
val callback: Observable.OnPropertyChangedCallback =
object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
- pendingAnswerError = null,
- inputAnswerAvailable = true
+ pendingAnswerError,
+ inputAnswerAvailable = true // Allow submission without arranging or merging items.
)
}
}
isAnswerAvailable.addOnPropertyChangedCallback(callback)
- isAnswerAvailable.set(true) // For drag drop submit button will be enabled by default.
+ errorMessage.addOnPropertyChangedCallback(callback)
+
+ // Initializing with default values so that submit button is enabled by default.
+ interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError = null,
+ inputAnswerAvailable = true
+ )
}
override fun onItemDragged(
@@ -98,6 +122,7 @@ class DragAndDropSortInteractionViewModel private constructor(
if (allowMultipleItemsInSamePosition) {
(adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems)
}
+ checkPendingAnswerError(AnswerErrorCategory.REAL_TIME)
}
fun onItemMoved(
@@ -129,6 +154,20 @@ class DragAndDropSortInteractionViewModel private constructor(
this@DragAndDropSortInteractionViewModel.writtenTranslationContext
}.build()
+ /**
+ * It checks the pending error for the current drag and drop sort interaction, and correspondingly
+ * updates the error string based on the specified error category.
+ */
+ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
+ pendingAnswerError = when (category) {
+ AnswerErrorCategory.REAL_TIME -> null
+ AnswerErrorCategory.SUBMIT_TIME ->
+ getSubmitTimeError().getErrorMessageFromStringRes(resourceHandler)
+ }
+ errorMessage.set(pendingAnswerError)
+ return pendingAnswerError
+ }
+
/** Returns an HTML list containing all of the HTML string elements as items in the list. */
private fun convertItemsToAnswer(htmlItems: List): ListOfSetsOfHtmlStrings {
return ListOfSetsOfHtmlStrings.newBuilder()
@@ -190,6 +229,13 @@ class DragAndDropSortInteractionViewModel private constructor(
(adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems)
}
+ private fun getSubmitTimeError(): DragAndDropSortInteractionError {
+ return if (_originalChoiceItems == _choiceItems)
+ DragAndDropSortInteractionError.EMPTY_INPUT
+ else
+ DragAndDropSortInteractionError.VALID
+ }
+
/** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */
class FactoryImpl @Inject constructor(
private val resourceHandler: AppLanguageResourceHandler,
diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt
index 132c988774b..06b6bb3a47c 100644
--- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt
@@ -105,12 +105,18 @@ class MathExpressionInteractionsViewModel private constructor(
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
errorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
pendingAnswerError,
- answerText.isNotEmpty()
+ inputAnswerAvailable = true // Allow blank answer submission.
)
}
}
errorMessage.addOnPropertyChangedCallback(callback)
isAnswerAvailable.addOnPropertyChangedCallback(callback)
+
+ // Initializing with default values so that submit button is enabled by default.
+ errorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError = null,
+ inputAnswerAvailable = true
+ )
}
override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply {
@@ -147,18 +153,16 @@ class MathExpressionInteractionsViewModel private constructor(
}.build()
override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
- if (answerText.isNotEmpty()) {
- pendingAnswerError = when (category) {
- // There's no support for real-time errors.
- AnswerErrorCategory.REAL_TIME -> null
- AnswerErrorCategory.SUBMIT_TIME -> {
- interactionType.computeSubmitTimeError(
- answerText.toString(), allowedVariables, resourceHandler
- )
- }
+ pendingAnswerError = when (category) {
+ // There's no support for real-time errors.
+ AnswerErrorCategory.REAL_TIME -> null
+ AnswerErrorCategory.SUBMIT_TIME -> {
+ interactionType.computeSubmitTimeError(
+ answerText.toString(), allowedVariables, resourceHandler
+ )
}
- errorMessage.set(pendingAnswerError)
}
+ errorMessage.set(pendingAnswerError)
return pendingAnswerError
}
@@ -290,7 +294,10 @@ class MathExpressionInteractionsViewModel private constructor(
}
private companion object {
- private enum class InteractionType(
+ /**
+ * Enum class representing different types of interactions in a mathematical expression input field.
+ */
+ enum class InteractionType(
val viewType: ViewType,
@StringRes val defaultHintTextStringId: Int,
val hasPlaceholder: Boolean,
@@ -420,6 +427,25 @@ class MathExpressionInteractionsViewModel private constructor(
allowedVariables: List,
appLanguageResourceHandler: AppLanguageResourceHandler
): String? {
+ if (answerText.isBlank()) {
+ return when (this) {
+ NUMERIC_EXPRESSION -> {
+ appLanguageResourceHandler.getStringInLocale(
+ R.string.numeric_expression_error_empty_input
+ )
+ }
+ ALGEBRAIC_EXPRESSION -> {
+ appLanguageResourceHandler.getStringInLocale(
+ R.string.algebraic_expression_error_empty_input
+ )
+ }
+ MATH_EQUATION -> {
+ appLanguageResourceHandler.getStringInLocale(
+ R.string.math_equation_error_empty_input
+ )
+ }
+ }
+ }
return when (val parseResult = parseAnswer(answerText, allowedVariables)) {
is MathParsingResult.Failure -> when (val error = parseResult.error) {
is DisabledVariablesInUseError -> {
diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt
index b3bdbba2ac0..7685e3673b9 100644
--- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt
@@ -2,6 +2,7 @@ package org.oppia.android.app.player.state.itemviewmodel
import android.text.Editable
import android.text.TextWatcher
+import androidx.annotation.StringRes
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import org.oppia.android.R
@@ -9,6 +10,7 @@ import org.oppia.android.app.model.Interaction
import org.oppia.android.app.model.InteractionObject
import org.oppia.android.app.model.UserAnswer
import org.oppia.android.app.model.WrittenTranslationContext
+import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
@@ -28,20 +30,43 @@ class TextInputViewModel private constructor(
) : StateItemViewModel(ViewType.TEXT_INPUT_INTERACTION), InteractionAnswerHandler {
var answerText: CharSequence = ""
val hintText: CharSequence = deriveHintText(interaction)
+ private var pendingAnswerError: String? = null
var isAnswerAvailable = ObservableField(false)
+ val errorMessage = ObservableField("")
init {
val callback: Observable.OnPropertyChangedCallback =
object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
- /* pendingAnswerError= */ null,
- answerText.isNotEmpty()
+ pendingAnswerError = pendingAnswerError,
+ inputAnswerAvailable = true // Allow submit on empty answer.
)
}
}
isAnswerAvailable.addOnPropertyChangedCallback(callback)
+ errorMessage.addOnPropertyChangedCallback(callback)
+
+ // Initializing with default values so that submit button is enabled by default.
+ interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError = null,
+ inputAnswerAvailable = true
+ )
+ }
+
+ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
+ return when (category) {
+ AnswerErrorCategory.REAL_TIME -> null
+ AnswerErrorCategory.SUBMIT_TIME -> {
+ TextParsingUiError.createForText(
+ answerText.toString()
+ ).createForText(resourceHandler)
+ }
+ }.also {
+ pendingAnswerError = it
+ errorMessage.set(it)
+ }
}
fun getAnswerTextWatcher(): TextWatcher {
@@ -55,6 +80,7 @@ class TextInputViewModel private constructor(
if (isAnswerTextAvailable != isAnswerAvailable.get()) {
isAnswerAvailable.set(isAnswerTextAvailable)
}
+ checkPendingAnswerError(AnswerErrorCategory.REAL_TIME)
}
override fun afterTextChanged(s: Editable) {
@@ -121,4 +147,22 @@ class TextInputViewModel private constructor(
)
}
}
+
+ private enum class TextParsingUiError(@StringRes private var error: Int?) {
+ /** Corresponds to non empty input. */
+ VALID(error = null),
+
+ /** Corresponds to empty input. */
+ EMPTY_INPUT(error = R.string.text_error_empty_input);
+
+ /** Returns the string corresponding to this error's string resources, or null if there is none. */
+ fun createForText(resourceHandler: AppLanguageResourceHandler): String? =
+ error?.let(resourceHandler::getStringInLocale)
+
+ companion object {
+ /** Returns the [TextParsingUiError] corresponding to the input. */
+ fun createForText(text: String): TextParsingUiError =
+ if (text.isEmpty()) EMPTY_INPUT else VALID
+ }
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt
index f033c023d61..20706aa12a3 100644
--- a/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt
@@ -9,34 +9,25 @@ import org.oppia.android.R
import org.oppia.android.app.activity.ActivityComponentImpl
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
import org.oppia.android.app.customview.interaction.NumericInputInteractionView
-import org.oppia.android.app.customview.interaction.TextInputInteractionView
import org.oppia.android.app.model.InputInteractionViewTestActivityParams
-import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.ALGEBRAIC_EXPRESSION
-import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.MATH_EQUATION
-import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.MATH_INTERACTION_TYPE_UNSPECIFIED
-import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.NUMERIC_EXPRESSION
-import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.UNRECOGNIZED
import org.oppia.android.app.model.Interaction
import org.oppia.android.app.model.UserAnswer
import org.oppia.android.app.model.WrittenTranslationContext
import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
-import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel
import org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel
import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel
import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.InteractionItemFactory
-import org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel
import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
import org.oppia.android.databinding.ActivityInputInteractionViewTestBinding
import org.oppia.android.util.extensions.getProtoExtra
import org.oppia.android.util.extensions.putProtoExtra
import javax.inject.Inject
-import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel.FactoryImpl.FactoryFactoryImpl as MathExpViewModelFactoryFactoryImpl
/**
* This is a dummy activity to test input interaction views.
- * It contains [NumericInputInteractionView],and [TextInputInteractionView].
+ * It contains [NumericInputInteractionView]
*/
class InputInteractionViewTestActivity :
InjectableAutoLocalizedAppCompatActivity(),
@@ -48,17 +39,8 @@ class InputInteractionViewTestActivity :
@Inject
lateinit var numericInputViewModelFactory: NumericInputViewModel.FactoryImpl
- @Inject
- lateinit var textInputViewModelFactory: TextInputViewModel.FactoryImpl
-
- @Inject
- lateinit var mathExpViewModelFactoryFactory: MathExpViewModelFactoryFactoryImpl
-
val numericInputViewModel by lazy { numericInputViewModelFactory.create() }
- val textInputViewModel by lazy { textInputViewModelFactory.create() }
-
- lateinit var mathExpressionViewModel: MathExpressionInteractionsViewModel
lateinit var writtenTranslationContext: WrittenTranslationContext
override fun onCreate(savedInstanceState: Bundle?) {
@@ -74,37 +56,8 @@ class InputInteractionViewTestActivity :
InputInteractionViewTestActivityParams.getDefaultInstance()
)
writtenTranslationContext = params.writtenTranslationContext
- when (params.mathInteractionType) {
- NUMERIC_EXPRESSION -> {
- mathExpressionViewModel =
- mathExpViewModelFactoryFactory
- .createFactoryForNumericExpression()
- .create(interaction = params.interaction)
- }
- ALGEBRAIC_EXPRESSION -> {
- mathExpressionViewModel =
- mathExpViewModelFactoryFactory
- .createFactoryForAlgebraicExpression()
- .create(interaction = params.interaction)
- }
- MATH_EQUATION -> {
- mathExpressionViewModel =
- mathExpViewModelFactoryFactory
- .createFactoryForMathEquation()
- .create(interaction = params.interaction)
- }
- MATH_INTERACTION_TYPE_UNSPECIFIED, UNRECOGNIZED, null -> {
- // Default to numeric expression arbitrarily (since something needs to be defined).
- mathExpressionViewModel =
- mathExpViewModelFactoryFactory
- .createFactoryForNumericExpression()
- .create(interaction = params.interaction)
- }
- }
binding.numericInputViewModel = numericInputViewModel
- binding.textInputViewModel = textInputViewModel
- binding.mathExpressionInteractionsViewModel = mathExpressionViewModel
}
fun getPendingAnswerErrorOnSubmitClick(v: View) {
diff --git a/app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt
new file mode 100644
index 00000000000..55219840ea9
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt
@@ -0,0 +1,147 @@
+package org.oppia.android.app.testing
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.databinding.DataBindingUtil
+import org.oppia.android.R
+import org.oppia.android.app.activity.ActivityComponentImpl
+import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
+import org.oppia.android.app.model.Interaction
+import org.oppia.android.app.model.MathExpressionInteractionsViewTestActivityParams
+import org.oppia.android.app.model.MathExpressionInteractionsViewTestActivityParams.MathInteractionType
+import org.oppia.android.app.model.UserAnswer
+import org.oppia.android.app.model.WrittenTranslationContext
+import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
+import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
+import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
+import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel
+import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel
+import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
+import org.oppia.android.databinding.ActivityMathExpressionInteractionViewTestBinding
+import org.oppia.android.util.extensions.getProtoExtra
+import org.oppia.android.util.extensions.putProtoExtra
+import javax.inject.Inject
+
+/**
+ * This is a dummy activity to test input interaction views.
+ * It contains [MathExpressionInteractionsView].
+ */
+class MathExpressionInteractionsViewTestActivity :
+ InjectableAutoLocalizedAppCompatActivity(),
+ StateKeyboardButtonListener,
+ InteractionAnswerErrorOrAvailabilityCheckReceiver,
+ InteractionAnswerReceiver {
+
+ private lateinit var binding:
+ ActivityMathExpressionInteractionViewTestBinding
+
+ /**
+ * Injects the [MathExpressionInteractionsViewModel.FactoryImpl] for creating
+ * [MathExpressionInteractionsViewModel] instances.
+ */
+ @Inject
+ lateinit var mathExpViewModelFactoryFactory:
+ MathExpressionInteractionsViewModel.FactoryImpl.FactoryFactoryImpl
+
+ /** The [MathExpressionInteractionsViewModel] instance. */
+ lateinit var mathExpressionViewModel: MathExpressionInteractionsViewModel
+
+ /** Gives access to the translation context. */
+ lateinit var writtenTranslationContext: WrittenTranslationContext
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ (activityComponent as ActivityComponentImpl).inject(this)
+ binding = DataBindingUtil.setContentView(
+ this, R.layout.activity_math_expression_interaction_view_test
+ )
+ val params =
+ intent.getProtoExtra(
+ TEST_ACTIVITY_PARAMS_ARGUMENT_KEY,
+ MathExpressionInteractionsViewTestActivityParams.getDefaultInstance()
+ )
+ writtenTranslationContext = params.writtenTranslationContext
+ when (params.mathInteractionType) {
+ MathInteractionType.NUMERIC_EXPRESSION -> {
+ mathExpressionViewModel =
+ mathExpViewModelFactoryFactory
+ .createFactoryForNumericExpression()
+ .create(interaction = params.interaction)
+ }
+ MathInteractionType.ALGEBRAIC_EXPRESSION -> {
+ mathExpressionViewModel =
+ mathExpViewModelFactoryFactory
+ .createFactoryForAlgebraicExpression()
+ .create(interaction = params.interaction)
+ }
+ MathInteractionType.MATH_EQUATION -> {
+ mathExpressionViewModel =
+ mathExpViewModelFactoryFactory
+ .createFactoryForMathEquation()
+ .create(interaction = params.interaction)
+ }
+ MathInteractionType.MATH_INTERACTION_TYPE_UNSPECIFIED,
+ MathInteractionType.UNRECOGNIZED, null -> {
+ // Default to numeric expression arbitrarily (since something needs to be defined).
+ mathExpressionViewModel =
+ mathExpViewModelFactoryFactory
+ .createFactoryForNumericExpression()
+ .create(interaction = params.interaction)
+ }
+ }
+
+ binding.mathExpressionInteractionsViewModel = mathExpressionViewModel
+ }
+
+ /** Checks submit-time errors. */
+ fun getPendingAnswerErrorOnSubmitClick(v: View) {
+ mathExpressionViewModel.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME)
+ }
+
+ override fun onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError: String?,
+ inputAnswerAvailable: Boolean
+ ) {
+ binding.submitButton.isEnabled = pendingAnswerError == null
+ }
+
+ override fun onAnswerReadyForSubmission(answer: UserAnswer) {
+ }
+
+ override fun onEditorAction(actionCode: Int) {
+ }
+
+ private inline fun StateItemViewModel
+ .InteractionItemFactory.create(
+ interaction: Interaction = Interaction.getDefaultInstance()
+ ): T {
+ return create(
+ entityId = "fake_entity_id",
+ hasConversationView = false,
+ interaction = interaction,
+ interactionAnswerReceiver = this@MathExpressionInteractionsViewTestActivity,
+ answerErrorReceiver = this@MathExpressionInteractionsViewTestActivity,
+ hasPreviousButton = false,
+ isSplitView = false,
+ writtenTranslationContext,
+ timeToStartNoticeAnimationMs = null
+ ) as T
+ }
+
+ companion object {
+ private const val TEST_ACTIVITY_PARAMS_ARGUMENT_KEY =
+ "MathExpressionInteractionsViewTestActivity.params"
+
+ /** Function to create intent for MathExpressionInteractionsViewTestActivity. */
+ fun createIntent(
+ context: Context,
+ extras: MathExpressionInteractionsViewTestActivityParams
+ ): Intent {
+ return Intent(context, MathExpressionInteractionsViewTestActivity::class.java).also {
+ it.putProtoExtra(TEST_ACTIVITY_PARAMS_ARGUMENT_KEY, extras)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity.kt
new file mode 100644
index 00000000000..5308bac3f8e
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity.kt
@@ -0,0 +1,89 @@
+package org.oppia.android.app.testing
+
+import android.os.Bundle
+import android.view.View
+import androidx.databinding.DataBindingUtil
+import org.oppia.android.R
+import org.oppia.android.app.activity.ActivityComponentImpl
+import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
+import org.oppia.android.app.customview.interaction.TextInputInteractionView
+import org.oppia.android.app.model.Interaction
+import org.oppia.android.app.model.UserAnswer
+import org.oppia.android.app.model.WrittenTranslationContext
+import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
+import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
+import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
+import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel
+import org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel
+import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
+import org.oppia.android.databinding.ActivityTextInputInteractionViewTestBinding
+import javax.inject.Inject
+
+/**
+ * This is a dummy activity to test input interaction views.
+ * It contains [TextInputInteractionView]
+ */
+class TextInputInteractionViewTestActivity :
+ InjectableAutoLocalizedAppCompatActivity(),
+ StateKeyboardButtonListener,
+ InteractionAnswerErrorOrAvailabilityCheckReceiver,
+ InteractionAnswerReceiver {
+
+ private lateinit var binding: ActivityTextInputInteractionViewTestBinding
+
+ @Inject
+ lateinit var textinputViewModelFactory: TextInputViewModel.FactoryImpl
+
+ /** Gives access to the [TextInputViewModel]. */
+ val textInputViewModel by lazy {
+ textinputViewModelFactory.create()
+ }
+
+ /** Gives access to the translation context. */
+ lateinit var writtenTranslationContext: WrittenTranslationContext
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ (activityComponent as ActivityComponentImpl).inject(this)
+ binding = DataBindingUtil.setContentView(
+ this, R.layout.activity_text_input_interaction_view_test
+ )
+
+ writtenTranslationContext = WrittenTranslationContext.getDefaultInstance()
+ binding.textInputViewModel = textInputViewModel
+ }
+
+ /** Checks submit-time errors. */
+ fun getPendingAnswerErrorOnSubmitClick(v: View) {
+ textInputViewModel.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME)
+ }
+
+ override fun onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError: String?,
+ inputAnswerAvailable: Boolean
+ ) {
+ }
+
+ override fun onAnswerReadyForSubmission(answer: UserAnswer) {
+ }
+
+ override fun onEditorAction(actionCode: Int) {
+ }
+
+ private inline fun
+ StateItemViewModel.InteractionItemFactory.create(
+ interaction: Interaction = Interaction.getDefaultInstance()
+ ): T {
+ return create(
+ entityId = "fake_entity_id",
+ hasConversationView = false,
+ interaction = interaction,
+ interactionAnswerReceiver = this@TextInputInteractionViewTestActivity,
+ answerErrorReceiver = this@TextInputInteractionViewTestActivity,
+ hasPreviousButton = false,
+ isSplitView = false,
+ writtenTranslationContext,
+ timeToStartNoticeAnimationMs = null
+ ) as T
+ }
+}
diff --git a/app/src/main/res/layout/activity_input_interaction_view_test.xml b/app/src/main/res/layout/activity_input_interaction_view_test.xml
index 780294fa6b4..c60eb2fec5e 100644
--- a/app/src/main/res/layout/activity_input_interaction_view_test.xml
+++ b/app/src/main/res/layout/activity_input_interaction_view_test.xml
@@ -11,18 +11,12 @@
name="numericInputViewModel"
type="org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel" />
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_text_input_interaction_view_test.xml b/app/src/main/res/layout/activity_text_input_interaction_view_test.xml
new file mode 100644
index 00000000000..10bb57bf9f8
--- /dev/null
+++ b/app/src/main/res/layout/activity_text_input_interaction_view_test.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/drag_drop_interaction_item.xml b/app/src/main/res/layout/drag_drop_interaction_item.xml
index 53a7074bcac..bcc6c1053ef 100644
--- a/app/src/main/res/layout/drag_drop_interaction_item.xml
+++ b/app/src/main/res/layout/drag_drop_interaction_item.xml
@@ -67,5 +67,12 @@
app:draggableData="@{viewModel.choiceItems}"
app:onDragEnded="@{(adapter) -> viewModel.onDragEnded(adapter)}"
app:onItemDrag="@{(indexFrom, indexTo, adapter) -> viewModel.onItemDragged(indexFrom, indexTo, adapter)}" />
+
+
diff --git a/app/src/main/res/layout/text_input_interaction_item.xml b/app/src/main/res/layout/text_input_interaction_item.xml
index e204f674cf6..3bb47724d91 100644
--- a/app/src/main/res/layout/text_input_interaction_item.xml
+++ b/app/src/main/res/layout/text_input_interaction_item.xml
@@ -1,10 +1,12 @@
-
+
+
+
@@ -22,8 +24,8 @@
+
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index df911829dad..81bca583bed 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -498,32 +498,20 @@
شروط الخدمة
باستخدام %s ، فإنك توافق على<br><oppia-noninteractive-policy link=\"tos\"> شروط الخدمة</oppia-noninteractive-policy> و<oppia-noninteractive-policy link=\"privacy\"> سياسة الخصوصية</oppia-noninteractive-policy> .
يرجى زيارة <a href=\"https://www.oppia.org/terms\">هذه الصفحة</a> للحصول على أحدث نسخة من هذه الشروط.
- ما هي %s؟
- من هو المشرف؟
- كيف يمكنني إنشاء ملف تعريف(حساب) جديد؟
- كيف يمكنني الحصول على التطبيق بلغتي؟
- وجدت خلل. كيف يمكنني الإبلاغ عنه؟
- لماذا هناك فقط دروس رياضيات؟
- هل ستقومون بإنشاء مزيد من الدروس؟
- لماذا لا يتم تحميل مشغل الاستكشاف؟
- لماذا لا يتم تشغيل الصوت الخاص بي؟
- كيف يمكنني حذف ملف التعريف(حساب)؟
- كيف يمكنني تحديث التطبيق؟
- كيف يمكنني تحديث نظام التشغيل Android الخاص بي؟
- لا أجد سؤالي هنا. ماذا الان؟
- <p>%1$s <i>\"أو-بي-يا\"</i>(فنلندية) - \"للتعلم\"</p><p><br></p><p>%1$sمهمتنا هي مساعدة أي شخص على تعلم أي شيء يريده بطريقة فعالة وممتعة.</p><p><br></p><p>من خلال إنشاء مجموعة من الدروس المجانية عالية الجودة والفعالة بشكل واضح بمساعدة معلمين من جميع أنحاء العالم ، تهدف %1$s إلى تزويد الطلاب بتعليم جيد - بغض النظر عن مكان وجودهم أو الموارد التقليدية التي يمكنهم الوصول إليها.</p><p><br></p><p>كطالب ، يمكنك أن تبدأ مغامرتك التعليمية من خلال تصفح الموضوعات المدرجة في الصفحة الرئيسية!</p>
- <p>المشرف هو المستخدم الرئيسي الذي يدير ملفات التعريف والإعدادات لكل ملف تعريف على حسابه. هم على الأرجح والدك أو معلمك أو وصي عليك الذي أنشأ هذا الملف الشخصي لك.</p><p><br></p><p>يمكن للمسؤولين إدارة الملفات الشخصية وتعيين أرقام التعريف الشخصية وتغيير الإعدادات الأخرى ضمن حساباتهم. بناءً على ملف التعريف الخاص بك ، قد تكون أذونات المسؤول مطلوبة لبعض الميزات مثل تنزيل الموضوعات وتغيير رقم التعريف الشخصي وغير ذلك.</p><p><br></p><p>لمعرفة من هو المسؤول لديك ، انتقل إلى منتقي الملف الشخصي. الملف الشخصي الأول المدرج ولديه \"المسؤول\" مكتوب باسمه هو المسؤول.</p>
- <p>إذا كانت هذه هي المرة الأولى التي تنشئ فيها ملفًا شخصيًا وليس لديك رقم تعريف شخصي: <ol><li> من منتقي الملف الشخصي ، اضغط على<strong> قم بإعداد ملفات تعريف متعددة</strong></li><li> قم بإنشاء رقم تعريف شخصي و<strong>احفظ</strong></li><li> املأ جميع البيانات للملف الشخصي.<ol><li>(اختياري) قم بتحميل صورة.</li> <li>إدخال اسم.</li> <li>(اختياري) قم بتعيين رقم تعريف شخصي مكون من 3 أرقام.</li></ol></li><li> اضغط<strong>إنشاء</strong> . تمت إضافة هذا الملف الشخصي إلى منتقي ملف التعريف الخاص بك!</li></ol></p><p> إذا قمت بإنشاء ملف تعريف من قبل ولديك رقم تعريف شخصي:<ol><li> من منتقي الملف الشخصي ، اضغط على<strong>إضافة الملف الشخصي</strong></li> <li> أدخل رقم التعريف الشخصي الخاص بك وانقر فوق<strong>إرسال</strong></li><li> املأ جميع الحقول للملف الشخصي. <ol> <li>(اختياري) قم بتحميل صورة.</li> <li>إدخال اسم.</li> <li>(اختياري) قم بتعيين رقم تعريف شخصي مكون من 3 أرقام. </li></ol></li><li> اضغط<strong>إنشاء</strong> . تمت إضافة هذا الملف الشخصي إلى منتقي ملف التعريف الخاص بك!</li></ol></p><br><p> ملاحظة: فقط ال<u>مدير</u> قادر على إدارة الملفات الشخصية.</p>
- تدعم حاليًا تطبيق %s اللغات التالية: الإنجليزية، البرتغالية البرازيلية، العربية، السواحيلية، والبيدجن النيجيري. اختر إحدى هذه اللغات من القائمة، تحت الخيارات. لطلب التطبيق بلغتك، يرجى الاتصال بنا على admin@oppia.org .
-
من شاشة البداية لتطبيق %s الخاص بك، اضغط على القائمة في الزاوية اليسرى العليا. اضغط على مشاركة التعليقات . اتبع التعليمات للإبلاغ عن الخلل أو مشاركة التعليقات.
- مهمة %1$s هي مساعدة المتعلمين في اكتساب المهارات الحياتية الضرورية. الرياضيات هي مهارة أساسية في الحياة اليومية. %1$s سيقدم دروسًا جديدة في العلوم ومواضيع أخرى قريبًا!
- نعم، %s سيقدم دروس جديدة في العلوم ومواضيع أخرى قريبًا. يرجى التحقق مرة أخرى للحصول على التحديثات!
- <p>إذا لم يتم تحميل مشغل الاستكشاف</p><p><br></p><p>تحقق لمعرفة ما إذا كان التطبيق محدثًا أم لا:</p><p> <ul> <li> انتقل إلى متجر Play وتأكد من تحديث التطبيق إلى أحدث إصدار </li></ul><p><br></p><p>تحقق من اتصالك بالإنترنت:</p><ul><li> إذا كان اتصالك بالإنترنت بطيئًا ، فحاول إعادة الاتصال بشبكة Wi-Fi أو الاتصال بشبكة أخرى. </li></ul><p>اطلب من المشرف التحقق من أجهزتهم واتصال الإنترنت:</p><ul><li> اطلب من المشرف استكشاف الأخطاء وإصلاحها باستخدام الخطوات المذكورة أعلاه </li></ul><p>أخبرنا إذا كنت لا تزال تواجه مشكلات في التحميل:</p><ul><li> أبلغ عن مشكلة عن طريق الاتصال بنا على admin@oppia.org. </li></ul>
- <p>إذا لم يتم تشغيل الصوت الخاص بك</p><p><br></p><p>تحقق لمعرفة ما إذا كان التطبيق محدثًا أم لا:</p><ul><li>انتقل إلى متجر Play وتأكد من تحديث التطبيق إلى أحدث إصدار</li></ul><p><br></p><p>تحقق من اتصالك بالإنترنت:</p><ul><li>إذا كان اتصالك بالإنترنت بطيئًا ، فحاول إعادة الاتصال بشبكة Wi-Fi أو الاتصال بشبكة أخرى. قد يتسبب الإنترنت البطيء في تحميل الصوت بشكل غير منتظم ، مما يجعل من الصعب تشغيله.</li></ul><p><br></p><p>اطلب من المسؤول التحقق من أجهزتهم واتصال الإنترنت:</p><ul><li>اطلب من المسؤول استكشاف الأخطاء وإصلاحها باستخدام الخطوات المذكورة أعلاه</li></ul><p><br></p><p>أخبرنا إذا كنت لا تزال تواجه مشكلات في التحميل:</p><ul><li>أبلغ عن مشكلة عن طريق الاتصال بنا على admin@oppia.org.</li></ul>
- <p>بمجرد حذف ملف التعريف:</p><ol><li>لا يمكن استعادة ملف التعريف.</li><li>سيتم حذف معلومات الملف الشخصي مثل الاسم والصور والتقدم بشكل دائم.</li></ol><p>لحذف ملف تعريف (باستثناء<u>المسؤول</u>):</p><ol><li> من الصفحة الرئيسية للمسؤول ، اضغط على زر القائمة أعلى اليسار.</li><li> اضغط على<strong>ضوابط المسؤول</strong>. </li><li> اضغط على<strong>تحرير ملفات التعريف</strong>.</li><li> اضغط على الملف الشخصي الذي ترغب في حذفه.</li><li> في الجزء السفلي من الشاشة ، انقر فوق<strong>حذف الملف الشخصي</strong>. </li><li> اضغط<strong>حذف</strong>لتأكيد الحذف. </li></ol><p><br></p><p> ملاحظة:<u>المسؤول</u>فقط هو القادر على إدارة الملفات الشخصية.</p>
-
افتح تطبيق متجر Google Play. ابحث عن تطبيق %s. اضغط على تحديث.
-
اضغط على تطبيق الإعدادات على هاتفك. اضغط على تحديث النظام. اضغط على تحديث النظام واتبع التعليمات لتحديث نظام التشغيل Android الخاص بك.
- <p> إذا لم تتمكن من العثور على سؤالك أو كنت ترغب في الإبلاغ عن خطأ ، فاتصل بنا على admin@oppia.org. </p>
+ كيف يمكنني إنشاء ملف تعريف(حساب) جديد؟
+ كيف يمكنني حذف ملف التعريف(حساب)؟
+ ما هي %s؟
+ من هو المشرف؟
+ لماذا لا يتم تحميل مشغل الاستكشاف؟
+ لماذا لا يتم تشغيل الصوت الخاص بي؟
+ لا أجد سؤالي هنا. ماذا الان؟
+ <p>إذا كانت هذه هي المرة الأولى التي تنشئ فيها ملفًا شخصيًا وليس لديك رقم تعريف شخصي: <ol><li> من منتقي الملف الشخصي ، اضغط على<strong> قم بإعداد ملفات تعريف متعددة</strong></li><li> قم بإنشاء رقم تعريف شخصي و<strong>احفظ</strong></li><li> املأ جميع البيانات للملف الشخصي.<ol><li>(اختياري) قم بتحميل صورة.</li> <li>إدخال اسم.</li> <li>(اختياري) قم بتعيين رقم تعريف شخصي مكون من 3 أرقام.</li></ol></li><li> اضغط<strong>إنشاء</strong> . تمت إضافة هذا الملف الشخصي إلى منتقي ملف التعريف الخاص بك!</li></ol></p><p> إذا قمت بإنشاء ملف تعريف من قبل ولديك رقم تعريف شخصي:<ol><li> من منتقي الملف الشخصي ، اضغط على<strong>إضافة الملف الشخصي</strong></li> <li> أدخل رقم التعريف الشخصي الخاص بك وانقر فوق<strong>إرسال</strong></li><li> املأ جميع الحقول للملف الشخصي. <ol> <li>(اختياري) قم بتحميل صورة.</li> <li>إدخال اسم.</li> <li>(اختياري) قم بتعيين رقم تعريف شخصي مكون من 3 أرقام. </li></ol></li><li> اضغط<strong>إنشاء</strong> . تمت إضافة هذا الملف الشخصي إلى منتقي ملف التعريف الخاص بك!</li></ol></p><br><p> ملاحظة: فقط ال<u>مدير</u> قادر على إدارة الملفات الشخصية.</p>
+ <p>بمجرد حذف ملف التعريف:</p><ol><li>لا يمكن استعادة ملف التعريف.</li><li>سيتم حذف معلومات الملف الشخصي مثل الاسم والصور والتقدم بشكل دائم.</li></ol><p>لحذف ملف تعريف (باستثناء<u>المسؤول</u>):</p><ol><li> من الصفحة الرئيسية للمسؤول ، اضغط على زر القائمة أعلى اليسار.</li><li> اضغط على<strong>ضوابط المسؤول</strong>. </li><li> اضغط على<strong>تحرير ملفات التعريف</strong>.</li><li> اضغط على الملف الشخصي الذي ترغب في حذفه.</li><li> في الجزء السفلي من الشاشة ، انقر فوق<strong>حذف الملف الشخصي</strong>. </li><li> اضغط<strong>حذف</strong>لتأكيد الحذف. </li></ol><p><br></p><p> ملاحظة:<u>المسؤول</u>فقط هو القادر على إدارة الملفات الشخصية.</p>
+ <p>%1$s <i>\"أو-بي-يا\"</i>(فنلندية) - \"للتعلم\"</p><p><br></p><p>%1$sمهمتنا هي مساعدة أي شخص على تعلم أي شيء يريده بطريقة فعالة وممتعة.</p><p><br></p><p>من خلال إنشاء مجموعة من الدروس المجانية عالية الجودة والفعالة بشكل واضح بمساعدة معلمين من جميع أنحاء العالم ، تهدف %1$s إلى تزويد الطلاب بتعليم جيد - بغض النظر عن مكان وجودهم أو الموارد التقليدية التي يمكنهم الوصول إليها.</p><p><br></p><p>كطالب ، يمكنك أن تبدأ مغامرتك التعليمية من خلال تصفح الموضوعات المدرجة في الصفحة الرئيسية!</p>
+ <p>المشرف هو المستخدم الرئيسي الذي يدير ملفات التعريف والإعدادات لكل ملف تعريف على حسابه. هم على الأرجح والدك أو معلمك أو وصي عليك الذي أنشأ هذا الملف الشخصي لك.</p><p><br></p><p>يمكن للمسؤولين إدارة الملفات الشخصية وتعيين أرقام التعريف الشخصية وتغيير الإعدادات الأخرى ضمن حساباتهم. بناءً على ملف التعريف الخاص بك ، قد تكون أذونات المسؤول مطلوبة لبعض الميزات مثل تنزيل الموضوعات وتغيير رقم التعريف الشخصي وغير ذلك.</p><p><br></p><p>لمعرفة من هو المسؤول لديك ، انتقل إلى منتقي الملف الشخصي. الملف الشخصي الأول المدرج ولديه \"المسؤول\" مكتوب باسمه هو المسؤول.</p>
+ <p>إذا لم يتم تحميل مشغل الاستكشاف</p><p><br></p><p>تحقق لمعرفة ما إذا كان التطبيق محدثًا أم لا:</p><p> <ul> <li> انتقل إلى متجر Play وتأكد من تحديث التطبيق إلى أحدث إصدار </li></ul><p><br></p><p>تحقق من اتصالك بالإنترنت:</p><ul><li> إذا كان اتصالك بالإنترنت بطيئًا ، فحاول إعادة الاتصال بشبكة Wi-Fi أو الاتصال بشبكة أخرى. </li></ul><p>اطلب من المشرف التحقق من أجهزتهم واتصال الإنترنت:</p><ul><li> اطلب من المشرف استكشاف الأخطاء وإصلاحها باستخدام الخطوات المذكورة أعلاه </li></ul><p>أخبرنا إذا كنت لا تزال تواجه مشكلات في التحميل:</p><ul><li> أبلغ عن مشكلة عن طريق الاتصال بنا على admin@oppia.org. </li></ul>
+ <p>إذا لم يتم تشغيل الصوت الخاص بك</p><p><br></p><p>تحقق لمعرفة ما إذا كان التطبيق محدثًا أم لا:</p><ul><li>انتقل إلى متجر Play وتأكد من تحديث التطبيق إلى أحدث إصدار</li></ul><p><br></p><p>تحقق من اتصالك بالإنترنت:</p><ul><li>إذا كان اتصالك بالإنترنت بطيئًا ، فحاول إعادة الاتصال بشبكة Wi-Fi أو الاتصال بشبكة أخرى. قد يتسبب الإنترنت البطيء في تحميل الصوت بشكل غير منتظم ، مما يجعل من الصعب تشغيله.</li></ul><p><br></p><p>اطلب من المسؤول التحقق من أجهزتهم واتصال الإنترنت:</p><ul><li>اطلب من المسؤول استكشاف الأخطاء وإصلاحها باستخدام الخطوات المذكورة أعلاه</li></ul><p><br></p><p>أخبرنا إذا كنت لا تزال تواجه مشكلات في التحميل:</p><ul><li>أبلغ عن مشكلة عن طريق الاتصال بنا على admin@oppia.org.</li></ul>
+ <p> إذا لم تتمكن من العثور على سؤالك أو كنت ترغب في الإبلاغ عن خطأ ، فاتصل بنا على admin@oppia.org. </p>
نشاط اختبار جزء تحرير ملف التعريف
يتحكم المسؤول في نشاط اختبار التجزئة
أكمل دراستك
diff --git a/app/src/main/res/values-pcm-rNG/strings.xml b/app/src/main/res/values-pcm-rNG/strings.xml
index 6ff2280d115..5e98c6d7d17 100644
--- a/app/src/main/res/values-pcm-rNG/strings.xml
+++ b/app/src/main/res/values-pcm-rNG/strings.xml
@@ -507,32 +507,32 @@
Terms of Service
By using %s, you dey agree to our <br> <oppia-noninteractive-policy link=\"tos\">Terms of Service</oppia-noninteractive-policy> and <oppia-noninteractive-policy link=\"privacy\">Privacy Policy</oppia-noninteractive-policy>.
Abeg visit <a href=\"https://www.oppia.org/terms\">dis page</a> for di latest version of dese terms.
- Wetin be %s?
- Who be Administrator?
- How I go fit create a new profile?
- How I fit get the app for my language?
- I see bug for di app. How I fit report am?
- Why only math lessons dey available?
- Una go dey create more lessons?
- Why di Exploration player no dey load?
- Why my audio no dey play?
- How I go delete a profile?
- How I fit update di app?
- How I fit update my Android OS?
- I no dey find my question here. What now?
- <p>%1$s <i>\"O-pee-yah\"</i> (Finnish) - \"to learn\"</p><p><br></p><p>%1$s\'s mission na to help anyone learn anything dey want in an effective and enjoyable way.</p><p><br></p><p>By creating a set of free, high-quality, demonstrably effective lessons with di help of educators from around di world, %1$s dey aim to provide students with quality education — regardless of where dem dey or di traditional resources wey dem get access to.</p><p><br></p><p>As a student, you fit start your learning adventure by browsing di topics listed on di Home Page!</p>
- <p>An Administrator na di main user wey dey manage profiles and settings for every profile on top their account. They fit be your parent, teacher, or guardian wey don create dis profile for you. </p><p><br></p><p>Administrators get di ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions fit dey required for some features such as changing your PIN, and more. </p><p><br></p><p>To see who your Administrator be, go di Profile Chooser. Di first profile fot di list and get \"Administrator\" written under their name na di Administrator. </p>
- <p>If na your first time creating a profile and not have a PIN:<ol><li>From di Profile Chooser, tap on <strong>Set up Multiple Profiles</strong>.</li><li>Create a PIN and <strong>Save</strong>.</li><li>Fill in all boxes for di profile.<ol><li>(Optional) Upload a photo.</li><li>Enter a name.</li><li>(Optional) Assign a 3-digit PIN.</li></ol></li><li>Tap <strong>Create</strong>. Dis profile go add to your Profile Chooser!</li></ol></p><p> If you don create a profile before and you get a PIN:<ol><li>From di Profile Chooser, tap on <strong>Add Profile</strong>. </li><li>Enter your PIN and tap <strong>Submit</strong>. </li><li>Fill in all boxes for di profile.<ol><li> (Optional) Upload a photo. </li><li> Enter a name. </li><li> (Optional) Assign a 3-digit PIN. </li></ol></li><li>Tap <strong>Create</strong>. Dis profile go add to your Profile Chooser!</li></ol></p><br><p>Note: Only di <u>Administrator</u> go dey able to manage profiles.</p>
- <p>The %s app dey support English, Brazilian Portuguese, Arabic, Swahili, and Nigerian Pidgin. Select one of these languages for di menu, under Options. To ask for the app for your language, abeg contact us for admin@oppia.org .</p>
- <p>From your %s app home screen, tap the menu for di top left corner. Tap Share feedback . Follow di instructions to report di bug or share feedback. </p>
- %1$s mission na to help learners gain necessary life skills. Math na essential skill for everyday life. %1$s go dey offer new lessons on science and other subjects very soon!
- Yes, %s go dey offer new lessons on science and other subjects very soon. Abeg check back for updates!
- <p>If di Exploration Player no dey load</p><p><br></p><p>Check to see if di app dey up to date:</p><p><ul><li> Go to di Play Store and make sure sey di app dey updated to di latest version </li></ul><p><br></p><p>Check your internet connection:</p><ul><li> If your internet connection dey slow, try re-connecting to your Wi-Fi network or connecting to a different network. </li></ul><p>Ask di Administrator to check their device and internet connection:</p><ul><li> Get di Administrator to troubleshoot using di steps above </li></ul><p>Let us know if you still dey get issues with loading:</p><ul><li> Report a problem by contacting us at admin@oppia.org. </li></ul>
- <p>If your audio no dey play</p><p><br></p><p>Check to see if di app dey up to date:</p><ul><li> Go to di Play Store and make sure sey di app dey updated to di latest version </li></ul><p><br></p><p>Check your internet connection:</p><ul><li> If your internet connection dey slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet fit cause di audio to load irregularly, and go make am difficult to play. </li></ul><p><br></p><p>Ask di Administrator to check their device and internet connection:</p><ul><li> Get di Administrator to troubleshoot using di steps for up</li></ul><p><br></p><p>Let us know if you still dey get issues with loading:</p><ul><li> Report a problem by contacting us at admin@oppia.org. </li></ul>
- <p>Once profile don delete:</p><ol><li>Di profile no fit dey recovered. </li><li> Profile information such as name, photos, and progress go permanently delete. </li></ol><p>To delete a profile (excluding the <u>Administrator\'s</u>):</p><ol><li> From di Administrator\'s Home Page, tap on di menu button on di top left. </li><li>Tap on <strong>Administrator Controls</strong>. </li><li>Tap on <strong>Edit Profiles</strong>. </li><li>Tap on di Profile wey you wan delete. </li><li>For di bottom of di screen, tap <strong>Profile Deletion</strong>. </li><li>Tap <strong>Delete</strong> to confirm deletion. </li></ol><p><br></p><p>Note: Only di <u>Administrator</u> go dey able to manage profiles.</p>
-
Open di Google Play Store app. Search for di %s app. Tap Update.
- Tap your phone\'s Settings app. Tap System updates. Tap System updates and follow di instructions to update your Android operating system.
- <p>If you no fit find your question or you go like to report a bug, contact us for admin@oppia.org.</p>
+ Wetin be %s?
+ Who be Administrator?
+ How I go fit create a new profile?
+ How I fit get the app for my language?
+ I see bug for di app. How I fit report am?
+ Why only math lessons dey available?
+ Una go dey create more lessons?
+ Why di Exploration player no dey load?
+ Why my audio no dey play?
+ How I go delete a profile?
+ How I fit update di app?
+ How I fit update my Android OS?
+ I no dey find my question here. What now?
+ <p>%1$s <i>\"O-pee-yah\"</i> (Finnish) - \"to learn\"</p><p><br></p><p>%1$s\'s mission na to help anyone learn anything dey want in an effective and enjoyable way.</p><p><br></p><p>By creating a set of free, high-quality, demonstrably effective lessons with di help of educators from around di world, %1$s dey aim to provide students with quality education — regardless of where dem dey or di traditional resources wey dem get access to.</p><p><br></p><p>As a student, you fit start your learning adventure by browsing di topics listed on di Home Page!</p>
+ <p>An Administrator na di main user wey dey manage profiles and settings for every profile on top their account. They fit be your parent, teacher, or guardian wey don create dis profile for you. </p><p><br></p><p>Administrators get di ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions fit dey required for some features such as changing your PIN, and more. </p><p><br></p><p>To see who your Administrator be, go di Profile Chooser. Di first profile fot di list and get \"Administrator\" written under their name na di Administrator. </p>
+ <p>If na your first time creating a profile and not have a PIN:<ol><li>From di Profile Chooser, tap on <strong>Set up Multiple Profiles</strong>.</li><li>Create a PIN and <strong>Save</strong>.</li><li>Fill in all boxes for di profile.<ol><li>(Optional) Upload a photo.</li><li>Enter a name.</li><li>(Optional) Assign a 3-digit PIN.</li></ol></li><li>Tap <strong>Create</strong>. Dis profile go add to your Profile Chooser!</li></ol></p><p> If you don create a profile before and you get a PIN:<ol><li>From di Profile Chooser, tap on <strong>Add Profile</strong>. </li><li>Enter your PIN and tap <strong>Submit</strong>. </li><li>Fill in all boxes for di profile.<ol><li> (Optional) Upload a photo. </li><li> Enter a name. </li><li> (Optional) Assign a 3-digit PIN. </li></ol></li><li>Tap <strong>Create</strong>. Dis profile go add to your Profile Chooser!</li></ol></p><br><p>Note: Only di <u>Administrator</u> go dey able to manage profiles.</p>
+ <p>The %s app dey support English, Brazilian Portuguese, Arabic, Swahili, and Nigerian Pidgin. Select one of these languages for di menu, under Options. To ask for the app for your language, abeg contact us for admin@oppia.org .</p>
+ <p>From your %s app home screen, tap the menu for di top left corner. Tap Share feedback . Follow di instructions to report di bug or share feedback. </p>
+ %1$s mission na to help learners gain necessary life skills. Math na essential skill for everyday life. %1$s go dey offer new lessons on science and other subjects very soon!
+ Yes, %s go dey offer new lessons on science and other subjects very soon. Abeg check back for updates!
+ <p>If di Exploration Player no dey load</p><p><br></p><p>Check to see if di app dey up to date:</p><p><ul><li> Go to di Play Store and make sure sey di app dey updated to di latest version </li></ul><p><br></p><p>Check your internet connection:</p><ul><li> If your internet connection dey slow, try re-connecting to your Wi-Fi network or connecting to a different network. </li></ul><p>Ask di Administrator to check their device and internet connection:</p><ul><li> Get di Administrator to troubleshoot using di steps above </li></ul><p>Let us know if you still dey get issues with loading:</p><ul><li> Report a problem by contacting us at admin@oppia.org. </li></ul>
+ <p>If your audio no dey play</p><p><br></p><p>Check to see if di app dey up to date:</p><ul><li> Go to di Play Store and make sure sey di app dey updated to di latest version </li></ul><p><br></p><p>Check your internet connection:</p><ul><li> If your internet connection dey slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet fit cause di audio to load irregularly, and go make am difficult to play. </li></ul><p><br></p><p>Ask di Administrator to check their device and internet connection:</p><ul><li> Get di Administrator to troubleshoot using di steps for up</li></ul><p><br></p><p>Let us know if you still dey get issues with loading:</p><ul><li> Report a problem by contacting us at admin@oppia.org. </li></ul>
+ <p>Once profile don delete:</p><ol><li>Di profile no fit dey recovered. </li><li> Profile information such as name, photos, and progress go permanently delete. </li></ol><p>To delete a profile (excluding the <u>Administrator\'s</u>):</p><ol><li> From di Administrator\'s Home Page, tap on di menu button on di top left. </li><li>Tap on <strong>Administrator Controls</strong>. </li><li>Tap on <strong>Edit Profiles</strong>. </li><li>Tap on di Profile wey you wan delete. </li><li>For di bottom of di screen, tap <strong>Profile Deletion</strong>. </li><li>Tap <strong>Delete</strong> to confirm deletion. </li></ol><p><br></p><p>Note: Only di <u>Administrator</u> go dey able to manage profiles.</p>
+
Open di Google Play Store app. Search for di %s app. Tap Update.
+ Tap your phone\'s Settings app. Tap System updates. Tap System updates and follow di instructions to update your Android operating system.
+ <p>If you no fit find your question or you go like to report a bug, contact us for admin@oppia.org.</p>
Profile Edit Fragment Test Activity
Administrator Controls Fragment Test Activity
Continue Studying
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 668870a2bcb..9711a238ba1 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -478,32 +478,20 @@
Termos de Serviço
Ao usar %s, você concorda com nossos <br> <oppia-noninteractive-policy link=\"tos\">Termos de Serviço</oppia-noninteractive-policy> e <oppia-noninteractive-policy link=\"privacy\">Política de Privacidade</oppia-noninteractive-policy>.
Por favor visite <a href=\"https://www.oppia.org/terms\">esta página</a> para acessar a última versão destes termos.
- O que é %s?
- Quem é um administrador?
- Como posso criar um novo perfil?
- Como posso obter o aplicativo no meu idioma?
- Achei um bug. Como posso reportar?
- Por que só existem aulas de matemática?
- Vocês vão criar mais lições?
- Por que a exploração não está carregando?
- Por que meu áudio não está tocando?
- Como posso deletar um perfil?
- Como eu atualizo o aplicativo?
- Como eu atualizo meu sistema operacional Android?
- Não consigo encontrar minha pergunta aqui. E agora?
- <p>%1$s <i>\"O-pee-yah\"</i> (Finnish) - \"aprender\"</p><p><br></p><p>%1$s tem a missão de ajudar qualquer pessoa a aprender o que quiser de uma forma eficaz e agradável.</p><p><br></p><p>Ao criar um conjunto de aulas gratuitas, de alta qualidade e comprovadamente eficazes com a ajuda de educadores de todo o mundo, %1$s visa proporcionar aos alunos uma educação de qualidade - independentemente de onde estejam ou a quais recursos tradicionais tenham acesso.</p><p><br></p><p>Como estudante, você pode começar sua aventura de aprendizado navegando pelos tópicos listados na página inicial!</p>
- <p>Um administrador é o usuário principal que gerencia perfis e configurações para cada perfil em sua conta. Provavelmente, eles são seus pais, professores ou responsáveis que criaram este perfil para você.</p><p><br></p><p>Os administradores podem gerenciar perfis, atribuir PINs e alterar outras configurações em suas contas. Dependendo do seu perfil, as permissões de administrador podem ser necessárias para determinados recursos, como download de tópicos, alteração do PIN e mais. </p><p><br></p><p>Para ver quem é o seu administrador, vá para o Seletor de perfil. O primeiro perfil listado e com \"Administrador\" escrito em seu nome é o Administrador. </p>
- <p>Se é a sua primeira vez criando um perfil e você não tem um PIN:<ol> <li> No Seletor de Perfil, toque em <strong>Configurar Múltiplos Perfis</strong>. </li> <li> Crie um PIN e <strong>Salvar</strong>. </li> <li> Preencha todos os campos do perfil. <ol> <li> (Opcional) Carregue uma foto. </li> <li> Insira um nome. </li> <li> (Opcional) Atribua um PIN de 3 dígitos. </li></ol></li><li> 4. Toque em <strong>Criar</strong>. Este perfil está adicionado ao seu Seletor de Perfil! </li></ol></p><p> Se você já criou um perfil antes e tem um PIN: <ol> <li>No Seletor de Perfil, toque em <strong>Adicionar Perfil</strong>. </li> <li> 2. Digite seu PIN e toque em <strong>Enviar</strong>. </li> <li> Preencha todos os campos do perfil. <ol> <li> (Opcional) Carregue uma foto. </li> <li> Insira um nome. </li> <li> (Opcional) Atribua um PIN de 3 dígitos. </li> </ol> </li> <li> 4. Toque em <strong>Criar</strong>. Este perfil está adicionado ao seu Seletor de Perfil! </li></ol></p><br><p> Nota: Apenas o <u>Administrador</u> pode gerenciar perfis.</p>
- O aplicativo %s atualmente suporta Inglês, Português Brasileiro, Árabe, Suaíli e Pidgin Nigeriano. Escolha uma dessas línguas no menu, em Opções. Para solicitar o aplicativo no seu idioma, entre em contato conosco em admin@oppia.org .
-
Na tela inicial do seu aplicativo %s, toque no menu no canto superior esquerdo. Toque em Compartilhar feedback . Siga as instruções para reportar o bug ou compartilhar feedback.
- A missão de %1$s é ajudar os aprendizes a adquirir habilidades necessárias para a vida. A matemática é uma habilidade essencial no dia a dia. %1$s estará oferecendo novas lições sobre ciência e outras disciplinas em breve!
- Sim, %s estará oferecendo novas lições sobre ciência e outras disciplinas em breve. Por favor, volte para conferir as atualizações!
- <p>Se a exploração não estiver carregando</p><p><br></p><p>Verifique se o aplicativo está atualizado:</p><p> <ul> <li> Acesse a Play Store e certifique-se de que o aplicativo esteja atualizado com a versão mais recente </li></ul><p><br></p><p>Verifique sua conexão com a internet:</p><ul><li> Se sua conexão com a Internet estiver lenta, tente se reconectar à rede Wi-Fi ou conectar-se a uma rede diferente. </li></ul><p>Peça ao administrador para verificar o dispositivo e a conexão com a Internet:</p><ul><li> Peça ao administrador para solucionar o problema usando as etapas acima </li></ul><p>Informe-nos se você ainda tiver problemas com o carregamento::</p><ul><li> Relate um problema entrando em contato conosco em admin@oppia.org. </li> </ul>
- <p>Se o seu áudio não estiver tocando</p><p><br></p><p>Verifique se o aplicativo está atualizado:</p><ul><li> Acesse a Play Store e certifique-se de que o aplicativo esteja atualizado com a versão mais recente </li></ul><p><br></p><p>Verifique sua conexão com a internet:</p><ul><li> Se sua conexão com a Internet estiver lenta, tente se reconectar à rede Wi-Fi ou conectar-se a uma rede diferente. A Internet lenta pode fazer com que o áudio carregue irregularmente, dificultando a reprodução. </li></ul><p><br></p><p>Peça ao administrador para verificar o dispositivo e a conexão com a Internet:</p><ul><li> Peça ao administrador para solucionar o problema usando as etapas acima</li></ul><p><br></p><p>Informe-nos se você ainda tiver problemas com o carregamento:</p><ul><li> Relate um problema entrando em contato conosco em admin@oppia.org. </li></ul>
- <p>Depois que um perfil é deletado:</p> <ol><li>O perfil não pode ser recuperado. </li> <li> As informações do perfil, como nome, fotos e progresso, serão excluídas permanentemente. </li></ol><p>Para deletar um perfil(excluindo o do <u>Administrador</u>):</p> <ol><li>Na página inicial do administrador, toque no botão de menu no canto superior esquerdo.</li> <li>Toque em <strong>Controles do Administrador</strong>.</li> <li>3. Toque em <strong>Editar Perfis</strong>.</li> <li>4. Toque no perfil que deseja excluir.</li> <li>5. Na parte inferior da tela, toque em <strong>Exclusão de Perfil</strong>.</li> <li>6. Toque em <strong>Deletar</strong> para confirmar a exclusão.</li></ol><p><br></p><p>Nota: Apenas o <u>Administrador</u> pode gerenciar perfis.</p>
-
Abra o aplicativo Google Play Store. Procure pelo aplicativo %s. Toque em Atualizar.
-
Toque no aplicativo Configurações do seu telefone. Toque em Atualizações do sistema. Toque em Atualizações do sistema e siga as instruções para atualizar o sistema operacional Android.
- <p>Se você não consegue encontrar sua pergunta ou gostaria de relatar um problema, entre em contato conosco em admin@oppia.org.</p>
+ O que é %s?
+ Quem é um administrador?
+ Como posso criar um novo perfil?
+ Por que a exploração não está carregando?
+ Por que meu áudio não está tocando?
+ Como posso deletar um perfil?
+ Não consigo encontrar minha pergunta aqui. E agora?
+ <p>%1$s <i>\"O-pee-yah\"</i> (Finnish) - \"aprender\"</p><p><br></p><p>%1$s tem a missão de ajudar qualquer pessoa a aprender o que quiser de uma forma eficaz e agradável.</p><p><br></p><p>Ao criar um conjunto de aulas gratuitas, de alta qualidade e comprovadamente eficazes com a ajuda de educadores de todo o mundo, %1$s visa proporcionar aos alunos uma educação de qualidade - independentemente de onde estejam ou a quais recursos tradicionais tenham acesso.</p><p><br></p><p>Como estudante, você pode começar sua aventura de aprendizado navegando pelos tópicos listados na página inicial!</p>
+ <p>Um administrador é o usuário principal que gerencia perfis e configurações para cada perfil em sua conta. Provavelmente, eles são seus pais, professores ou responsáveis que criaram este perfil para você.</p><p><br></p><p>Os administradores podem gerenciar perfis, atribuir PINs e alterar outras configurações em suas contas. Dependendo do seu perfil, as permissões de administrador podem ser necessárias para determinados recursos, como download de tópicos, alteração do PIN e mais. </p><p><br></p><p>Para ver quem é o seu administrador, vá para o Seletor de perfil. O primeiro perfil listado e com \"Administrador\" escrito em seu nome é o Administrador. </p>
+ <p>Se é a sua primeira vez criando um perfil e você não tem um PIN:<ol> <li> No Seletor de Perfil, toque em <strong>Configurar Múltiplos Perfis</strong>. </li> <li> Crie um PIN e <strong>Salvar</strong>. </li> <li> Preencha todos os campos do perfil. <ol> <li> (Opcional) Carregue uma foto. </li> <li> Insira um nome. </li> <li> (Opcional) Atribua um PIN de 3 dígitos. </li></ol></li><li> 4. Toque em <strong>Criar</strong>. Este perfil está adicionado ao seu Seletor de Perfil! </li></ol></p><p> Se você já criou um perfil antes e tem um PIN: <ol> <li>No Seletor de Perfil, toque em <strong>Adicionar Perfil</strong>. </li> <li> 2. Digite seu PIN e toque em <strong>Enviar</strong>. </li> <li> Preencha todos os campos do perfil. <ol> <li> (Opcional) Carregue uma foto. </li> <li> Insira um nome. </li> <li> (Opcional) Atribua um PIN de 3 dígitos. </li> </ol> </li> <li> 4. Toque em <strong>Criar</strong>. Este perfil está adicionado ao seu Seletor de Perfil! </li></ol></p><br><p> Nota: Apenas o <u>Administrador</u> pode gerenciar perfis.</p>
+ <p>Se a exploração não estiver carregando</p><p><br></p><p>Verifique se o aplicativo está atualizado:</p><p> <ul> <li> Acesse a Play Store e certifique-se de que o aplicativo esteja atualizado com a versão mais recente </li></ul><p><br></p><p>Verifique sua conexão com a internet:</p><ul><li> Se sua conexão com a Internet estiver lenta, tente se reconectar à rede Wi-Fi ou conectar-se a uma rede diferente. </li></ul><p>Peça ao administrador para verificar o dispositivo e a conexão com a Internet:</p><ul><li> Peça ao administrador para solucionar o problema usando as etapas acima </li></ul><p>Informe-nos se você ainda tiver problemas com o carregamento::</p><ul><li> Relate um problema entrando em contato conosco em admin@oppia.org. </li> </ul>
+ <p>Se o seu áudio não estiver tocando</p><p><br></p><p>Verifique se o aplicativo está atualizado:</p><ul><li> Acesse a Play Store e certifique-se de que o aplicativo esteja atualizado com a versão mais recente </li></ul><p><br></p><p>Verifique sua conexão com a internet:</p><ul><li> Se sua conexão com a Internet estiver lenta, tente se reconectar à rede Wi-Fi ou conectar-se a uma rede diferente. A Internet lenta pode fazer com que o áudio carregue irregularmente, dificultando a reprodução. </li></ul><p><br></p><p>Peça ao administrador para verificar o dispositivo e a conexão com a Internet:</p><ul><li> Peça ao administrador para solucionar o problema usando as etapas acima</li></ul><p><br></p><p>Informe-nos se você ainda tiver problemas com o carregamento:</p><ul><li> Relate um problema entrando em contato conosco em admin@oppia.org. </li></ul>
+ <p>Depois que um perfil é deletado:</p> <ol><li>O perfil não pode ser recuperado. </li> <li> As informações do perfil, como nome, fotos e progresso, serão excluídas permanentemente. </li></ol><p>Para deletar um perfil(excluindo o do <u>Administrador</u>):</p> <ol><li>Na página inicial do administrador, toque no botão de menu no canto superior esquerdo.</li> <li>Toque em <strong>Controles do Administrador</strong>.</li> <li>3. Toque em <strong>Editar Perfis</strong>.</li> <li>4. Toque no perfil que deseja excluir.</li> <li>5. Na parte inferior da tela, toque em <strong>Exclusão de Perfil</strong>.</li> <li>6. Toque em <strong>Deletar</strong> para confirmar a exclusão.</li></ol><p><br></p><p>Nota: Apenas o <u>Administrador</u> pode gerenciar perfis.</p>
+ <p>Se você não consegue encontrar sua pergunta ou gostaria de relatar um problema, entre em contato conosco em admin@oppia.org.</p>
Atividade de Teste de Fragmento de Edição de Perfil
Controle Administrativo da Atividade de Teste de Fragmento
Continuar estudando
diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml
index 2c697434dc8..049ebac057a 100644
--- a/app/src/main/res/values-sw/strings.xml
+++ b/app/src/main/res/values-sw/strings.xml
@@ -411,30 +411,30 @@
Habari ya asubuhi,
Habari ya mchana,
Habari ya jioni,
- %s ni nini?
- Msimamizi ni nani?
- Ninawezaje kuunda wasifu mpya?
- Ninawezaje kupata programu kwa lugha yangu?
- Nimegundua mdudu. Ninawezaje kuripoti?
- Kwa nini kuna masomo ya hesabu tu?
- Je, mtatengeneza masomo zaidi?
- Kwa nini kicheza Uchunguzi hakipakii?
- Kwa nini sauti yangu haichezwi?
- Ninawezaje kufuta wasifu?
- Ni jinsi gani naweza kusasisha programu?
- Jinsi gani naweza kusasisha mfumo wangu wa Android?
- Sijapata swali langu hapa. Nini sasa?
- <p>%1$s <i>\"O-pee-yah\"</i> (Kifini) - \"kujifunza\"</p><p><br></p><p>%1$s\'s dhamira ni kumsaidia mtu yeyote kujifunza chochote anachotaka kwa njia bora na ya kufurahisha.</p><p><br></p><p>Kwa kuunda seti ya masomo yasiyolipishwa, ya ubora wa juu, na yenye matokeo kwa usaidizi. ya waelimishaji kutoka duniani kote, %1$s inalenga kuwapa wanafunzi elimu bora — bila kujali walipo au ni nyenzo gani za jadi wanazoweza kufikia.</p><p><br></p><p> Kama mwanafunzi, unaweza kuanza safari yako ya kujifunza kwa kuvinjari mada zilizoorodheshwa kwenye Ukurasa wa Mwanzo!</p>
- <p>Msimamizi ndiye mtumiaji mkuu anayedhibiti wasifu na mipangilio ya kila wasifu kwenye akaunti yake.Uwezekano mkubwa ni mzazi, mwalimu au mlezi wako aliyekuundia wasifu huu. </p><p><br></p><p>Wasimamizi wana uwezo wa kudhibiti wasifu, kugawa Nambari ya Siri, na kubadilisha mipangilio mingine chini ya akaunti yao. Kulingana na wasifu wako, ruhusa za Msimamizi zinaweza kuhitajika kwa vipengele fulani kama vile kupakua Mada, kubadilisha Nambari yako ya Siri na zaidi. </p><p><br></p><p>Ili kujua Msimamizi wako ni nani, nenda kwa Kichagua Wasifu. Wasifu wa kwanza ulioorodheshwa na una \"Msimamizi\" iliyoandikwa chini ya jina lake ni Msimamizi. </p>
- <p>Ikiwa ni mara yako ya kwanza kuunda wasifu na huna Nambari ya Siri: <ol> <li> Kutoka kwa Kichagua Wasifu, gusa <strong>Weka Wasifu Nyingi</strong>. </li> <li> Unda Nambari ya Siri na <strong>Hifadhi</strong>. </li> <li> Jaza sehemu zote za wasifu. <ol><li> (Si lazima) Pakia picha. </li> <li> Weka jina. </li> <li> (Si lazima) Weka Nambari ya Siri yenye tarakimu 3.</li></ol></li><li>Gusa <strong>Unda</strong>. Wasifu huu umeongezwa kwa Kichagua Wasifu wako! </li></ol></p><p> Ikiwa umeunda wasifu hapo awali na una Nambari ya Siri: <ol><li> Kutoka kwa Kichagua Wasifu, gusa <strong>Ongeza Wasifu</strong>. </li><li>Weka Nambari yako ya Siri na uguse <strong>Wasilisha</strong>. </li><li> Jaza sehemu zote za wasifu. <ol><li> (Si lazima) Pakia picha. </li> <li> Weka jina. </li> <li> (Si lazima) Weka Nambari ya Siri yenye tarakimu 3. </li></ol></li><li> Gusa <strong>Unda</strong>. Wasifu huu umeongezwa kwa Kichagua Wasifu wako! </li></ol></p><br><p> Kumbuka: <u>Msimamizi pekee</u> ndiye anayeweza kudhibiti wasifu.</p>
- Programu ya %s kwa sasa inasaidia Kiingereza, Kireno cha Brazil, Kiarabu, Kiswahili, na Kipidgin cha Nigeria. Chagua moja ya lugha hizi kwenye menyu, chini ya Chaguo. Ili kuomba programu kwa lugha yako, tafadhali wasiliana nasi kwa admin@oppia.org .
-
Kutoka kwenye skrini kuu ya programu yako ya %s, bonyeza menyu kwenye kona ya juu kushoto. Bonyeza Shiriki Maoni . Fuata maagizo ya kuripoti mdudu au kushiriki maoni.
- Malengo ya %1$s ni kusaidia wanafunzi kupata stadi muhimu za maisha. Hisabati ni stadi muhimu katika maisha ya kila siku. %1$s itatoa masomo mapya kuhusu sayansi na masomo mengine hivi karibuni!
- Ndiyo, %s itatoa masomo mapya kuhusu sayansi na masomo mengine hivi karibuni. Tafadhali rudia kwa ajili ya habari mpya!
- <p>Ikiwa Kicheza Ugunduzi hakipakii</p><p><br></p><p>Angalia kama programu imesasishwa:</p><p> <ul> <li> Nenda kwenye Hifadhi ya Michezo na uhakikishe kuwa programu imesasishwa hadi toleo lake jipya zaidi </li> </ul> <p><br></p><p>Angalia muunganisho wako wa mtandao:</p><ul><li> Ikiwa muunganisho wako wa mtandao ni wa polepole, jaribu kuunganisha tena kwenye mtandao wako wa Wi-Fi au unganisha kwenye mtandao tofauti. </li></ul><p>Uliza Msimamizi aangalie kifaa chake na muunganisho wa mtandao:</p><ul><li> Pata Msimamizi kusuluhisha kwa kutumia hatua hapo juu </li></ul><p>Tujulishe ikiwa bado una matatizo ya upakiaji:</p><ul><li> Ripoti tatizo kwa kuwasiliana nasi kwa admin@oppia.org. </li></ul>
- <p>Ikiwa sauti yako haichezi</p><p><br></p><p>Angalia ili kuona kama programu imesasishwa:</p><ul><li> Nenda kwenye Hifadhi ya Michezo na uhakikishe kuwa programu imesasishwa hadi toleo lake jipya zaidi </li></ul><p><br></p><p>Angalia muunganisho wako wa mtandao:</p><ul><li> Iwapo muunganisho wako wa mtandao ni wa polepole, jaribu kuunganisha tena kwenye mtandao wako wa Wi-Fi au unganisha kwenye mtandao tofauti. Mtandao wa polepole unaweza kusababisha sauti kupakia kwa njia isiyo ya kawaida, na kuifanya iwe vigumu kucheza. </li></ul><p><br></p><p>Uliza Msimamizi aangalie kifaa chake na muunganisho wa mtandao:</p><ul><li> Wasiliana na Msimamizi ili asuluhishe kwa kutumia hatua hapo juu</li></ul><p><br></p><p>Tujulishe ikiwa bado una matatizo ya upakiaji:</p><ul><li> Ripoti tatizo kwa kuwasiliana nasi kwa admin@oppia.org. </li></ul>
- <p>Wasifu unapofutwa:</p><ol><li> Wasifu hauwezi kurejeshwa. </li><li> Taarifa ya wasifu kama vile jina, picha na maendeleo yatafutwa kabisa. </li></ol><p>Ili kufuta wasifu (bila kujumuisha <u>Msimamizi</u>):</p> <ol><li> Kutoka kwa Ukurasa wa Mwanzo wa Msimamizi, gusa kitufe cha menyu kilicho upande wa juu kushoto. </li><li> Gusa <strong>Vidhibiti vya Msimamizi</strong>. </li><li> Gusa <strong>Hariri Wasifu</strong>. </li><li> Gonga Wasifu ambao ungependa kufuta. </li><li> Katika sehemu ya chini ya skrini, gusa <strong>Ufutaji wa Wasifu</strong>. </li><li> Gusa <strong>Futa</strong> ili kuthibitisha kufuta.</li></ol><p><br></p><p>Kumbuka: <u>Msimamizi</u> pekee ndiye anayeweza kudhibiti wasifu.</p>
-
Fungua programu ya Duka la Google Play. Tafuta programu ya %s. Bonyeza Sasisha.
-
Bonyeza programu ya Mipangilio kwenye simu yako. Bonyeza Sasisho la mfumo. Bonyeza Sasisho la mfumo na fuata maagizo ya kusasisha mfumo wako wa uendeshaji wa Android.
- <p>Ikiwa huwezi kupata swali lako au ungependa kuripoti hitilafu, wasiliana nasi kwa admin@oppia.org.</p>
+ %s ni nini?
+ Msimamizi ni nani?
+ Ninawezaje kuunda wasifu mpya?
+ Ninawezaje kupata programu kwa lugha yangu?
+ Nimegundua mdudu. Ninawezaje kuripoti?
+ Kwa nini kuna masomo ya hesabu tu?
+ Je, mtatengeneza masomo zaidi?
+ Kwa nini kicheza Uchunguzi hakipakii?
+ Kwa nini sauti yangu haichezwi?
+ Ninawezaje kufuta wasifu?
+ Ni jinsi gani naweza kusasisha programu?
+ Jinsi gani naweza kusasisha mfumo wangu wa Android?
+ Sijapata swali langu hapa. Nini sasa?
+ <p>%1$s <i>\"O-pee-yah\"</i> (Kifini) - \"kujifunza\"</p><p><br></p><p>%1$s\'s dhamira ni kumsaidia mtu yeyote kujifunza chochote anachotaka kwa njia bora na ya kufurahisha.</p><p><br></p><p>Kwa kuunda seti ya masomo yasiyolipishwa, ya ubora wa juu, na yenye matokeo kwa usaidizi. ya waelimishaji kutoka duniani kote, %1$s inalenga kuwapa wanafunzi elimu bora — bila kujali walipo au ni nyenzo gani za jadi wanazoweza kufikia.</p><p><br></p><p> Kama mwanafunzi, unaweza kuanza safari yako ya kujifunza kwa kuvinjari mada zilizoorodheshwa kwenye Ukurasa wa Mwanzo!</p>
+ <p>Msimamizi ndiye mtumiaji mkuu anayedhibiti wasifu na mipangilio ya kila wasifu kwenye akaunti yake.Uwezekano mkubwa ni mzazi, mwalimu au mlezi wako aliyekuundia wasifu huu. </p><p><br></p><p>Wasimamizi wana uwezo wa kudhibiti wasifu, kugawa Nambari ya Siri, na kubadilisha mipangilio mingine chini ya akaunti yao. Kulingana na wasifu wako, ruhusa za Msimamizi zinaweza kuhitajika kwa vipengele fulani kama vile kupakua Mada, kubadilisha Nambari yako ya Siri na zaidi. </p><p><br></p><p>Ili kujua Msimamizi wako ni nani, nenda kwa Kichagua Wasifu. Wasifu wa kwanza ulioorodheshwa na una \"Msimamizi\" iliyoandikwa chini ya jina lake ni Msimamizi. </p>
+ <p>Ikiwa ni mara yako ya kwanza kuunda wasifu na huna Nambari ya Siri: <ol> <li> Kutoka kwa Kichagua Wasifu, gusa <strong>Weka Wasifu Nyingi</strong>. </li> <li> Unda Nambari ya Siri na <strong>Hifadhi</strong>. </li> <li> Jaza sehemu zote za wasifu. <ol><li> (Si lazima) Pakia picha. </li> <li> Weka jina. </li> <li> (Si lazima) Weka Nambari ya Siri yenye tarakimu 3.</li></ol></li><li>Gusa <strong>Unda</strong>. Wasifu huu umeongezwa kwa Kichagua Wasifu wako! </li></ol></p><p> Ikiwa umeunda wasifu hapo awali na una Nambari ya Siri: <ol><li> Kutoka kwa Kichagua Wasifu, gusa <strong>Ongeza Wasifu</strong>. </li><li>Weka Nambari yako ya Siri na uguse <strong>Wasilisha</strong>. </li><li> Jaza sehemu zote za wasifu. <ol><li> (Si lazima) Pakia picha. </li> <li> Weka jina. </li> <li> (Si lazima) Weka Nambari ya Siri yenye tarakimu 3. </li></ol></li><li> Gusa <strong>Unda</strong>. Wasifu huu umeongezwa kwa Kichagua Wasifu wako! </li></ol></p><br><p> Kumbuka: <u>Msimamizi pekee</u> ndiye anayeweza kudhibiti wasifu.</p>
+ Programu ya %s kwa sasa inasaidia Kiingereza, Kireno cha Brazil, Kiarabu, Kiswahili, na Kipidgin cha Nigeria. Chagua moja ya lugha hizi kwenye menyu, chini ya Chaguo. Ili kuomba programu kwa lugha yako, tafadhali wasiliana nasi kwa admin@oppia.org .
+
Kutoka kwenye skrini kuu ya programu yako ya %s, bonyeza menyu kwenye kona ya juu kushoto. Bonyeza Shiriki Maoni . Fuata maagizo ya kuripoti mdudu au kushiriki maoni.
+ Malengo ya %1$s ni kusaidia wanafunzi kupata stadi muhimu za maisha. Hisabati ni stadi muhimu katika maisha ya kila siku. %1$s itatoa masomo mapya kuhusu sayansi na masomo mengine hivi karibuni!
+ Ndiyo, %s itatoa masomo mapya kuhusu sayansi na masomo mengine hivi karibuni. Tafadhali rudia kwa ajili ya habari mpya!
+ <p>Ikiwa Kicheza Ugunduzi hakipakii</p><p><br></p><p>Angalia kama programu imesasishwa:</p><p> <ul> <li> Nenda kwenye Hifadhi ya Michezo na uhakikishe kuwa programu imesasishwa hadi toleo lake jipya zaidi </li> </ul> <p><br></p><p>Angalia muunganisho wako wa mtandao:</p><ul><li> Ikiwa muunganisho wako wa mtandao ni wa polepole, jaribu kuunganisha tena kwenye mtandao wako wa Wi-Fi au unganisha kwenye mtandao tofauti. </li></ul><p>Uliza Msimamizi aangalie kifaa chake na muunganisho wa mtandao:</p><ul><li> Pata Msimamizi kusuluhisha kwa kutumia hatua hapo juu </li></ul><p>Tujulishe ikiwa bado una matatizo ya upakiaji:</p><ul><li> Ripoti tatizo kwa kuwasiliana nasi kwa admin@oppia.org. </li></ul>
+ <p>Ikiwa sauti yako haichezi</p><p><br></p><p>Angalia ili kuona kama programu imesasishwa:</p><ul><li> Nenda kwenye Hifadhi ya Michezo na uhakikishe kuwa programu imesasishwa hadi toleo lake jipya zaidi </li></ul><p><br></p><p>Angalia muunganisho wako wa mtandao:</p><ul><li> Iwapo muunganisho wako wa mtandao ni wa polepole, jaribu kuunganisha tena kwenye mtandao wako wa Wi-Fi au unganisha kwenye mtandao tofauti. Mtandao wa polepole unaweza kusababisha sauti kupakia kwa njia isiyo ya kawaida, na kuifanya iwe vigumu kucheza. </li></ul><p><br></p><p>Uliza Msimamizi aangalie kifaa chake na muunganisho wa mtandao:</p><ul><li> Wasiliana na Msimamizi ili asuluhishe kwa kutumia hatua hapo juu</li></ul><p><br></p><p>Tujulishe ikiwa bado una matatizo ya upakiaji:</p><ul><li> Ripoti tatizo kwa kuwasiliana nasi kwa admin@oppia.org. </li></ul>
+ <p>Wasifu unapofutwa:</p><ol><li> Wasifu hauwezi kurejeshwa. </li><li> Taarifa ya wasifu kama vile jina, picha na maendeleo yatafutwa kabisa. </li></ol><p>Ili kufuta wasifu (bila kujumuisha <u>Msimamizi</u>):</p> <ol><li> Kutoka kwa Ukurasa wa Mwanzo wa Msimamizi, gusa kitufe cha menyu kilicho upande wa juu kushoto. </li><li> Gusa <strong>Vidhibiti vya Msimamizi</strong>. </li><li> Gusa <strong>Hariri Wasifu</strong>. </li><li> Gonga Wasifu ambao ungependa kufuta. </li><li> Katika sehemu ya chini ya skrini, gusa <strong>Ufutaji wa Wasifu</strong>. </li><li> Gusa <strong>Futa</strong> ili kuthibitisha kufuta.</li></ol><p><br></p><p>Kumbuka: <u>Msimamizi</u> pekee ndiye anayeweza kudhibiti wasifu.</p>
+
Fungua programu ya Duka la Google Play. Tafuta programu ya %s. Bonyeza Sasisha.
+
Bonyeza programu ya Mipangilio kwenye simu yako. Bonyeza Sasisho la mfumo. Bonyeza Sasisho la mfumo na fuata maagizo ya kusasisha mfumo wako wa uendeshaji wa Android.
+ <p>Ikiwa huwezi kupata swali lako au ungependa kuripoti hitilafu, wasiliana nasi kwa admin@oppia.org.</p>
diff --git a/app/src/main/res/values/faqs.xml b/app/src/main/res/values/faqs.xml
index 1c8e0af249c..c7f3706c359 100644
--- a/app/src/main/res/values/faqs.xml
+++ b/app/src/main/res/values/faqs.xml
@@ -1,32 +1,34 @@
- - @string/faq_question_1
- - @string/faq_question_2
- - @string/faq_question_3
- - @string/faq_question_4
- - @string/faq_question_5
- - @string/faq_question_6
- - @string/faq_question_7
- - @string/faq_question_9
- - @string/faq_question_10
- - @string/faq_question_11
- - @string/faq_question_12
- - @string/faq_question_13
+ - @string/faq_question_whats_oppia
+ - @string/faq_question_whos_an_admin
+ - @string/faq_question_create_profile
+ - @string/faq_question_app_language
+ - @string/faq_question_bug_reporting
+ - @string/faq_question_math_lessons
+ - @string/faq_question_more_lessons
+ - @string/faq_question_exploration_player
+ - @string/faq_question_audio_not_playing
+ - @string/faq_question_delete_profile
+ - @string/faq_question_update_app
+ - @string/faq_question_update_os
+ - @string/faq_question_cant_find_question
- - @string/faq_answer_1
- - @string/faq_answer_2
- - @string/faq_answer_3
- - @string/faq_answer_4
- - @string/faq_answer_5
- - @string/faq_answer_6
- - @string/faq_answer_7
- - @string/faq_answer_9
- - @string/faq_answer_10
- - @string/faq_answer_11
- - @string/faq_answer_12
- - @string/faq_answer_13
+ - @string/faq_answer_whats_oppia
+ - @string/faq_answer_whos_an_admin
+ - @string/faq_answer_create_profile
+ - @string/faq_answer_app_language
+ - @string/faq_answer_bug_reporting
+ - @string/faq_answer_math_lessons
+ - @string/faq_answer_more_lessons
+ - @string/faq_answer_exploration_player
+ - @string/faq_answer_audio_not_playing
+ - @string/faq_answer_delete_profile
+ - @string/faq_answer_update_app
+ - @string/faq_answer_update_os
+ - @string/faq_answer_cant_find_question
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5a836ba49a4..e17d0da9ebd 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -104,8 +104,11 @@
Enter a number.
Write numbers with units here.
Type an expression here, using only numbers.
+ Enter a numeric expression to continue.
Type an expression here.
+ Enter an algebraic expression to continue.
Type an equation here.
+ Enter an equation to continue.
Please remove the spaces between numbers in your answer.
Please close or remove parentheses.
Please remove the parentheses around the whole answer: \'%s\'.
@@ -178,6 +181,7 @@
Number of terms is not equal to the required terms.
Ratios cannot have 0 as an element.
Enter a ratio to continue.
+ Enter text to continue.
Choose an answer to continue.
Unknown size
%s Bytes
@@ -449,6 +453,7 @@
Return to lesson
Explanation:
If two items are equal, merge them.
+ Arrange the boxes to continue.
Link to item %s
Unlink items at %s
Move item down to %s
@@ -541,40 +546,39 @@
Policy Page
Privacy Policy
- this page for the latest version of this privacy policy.]]>
+ Please visit <a href="https://www.oppia.org/privacy-policy">this page</a> for the latest version of this privacy policy.
Terms of Service
- Terms of Service and Privacy Policy .]]>
- this page for the latest version of these terms.]]>
+ By using %s, you agree to our <br> <oppia-noninteractive-policy link="tos">Terms of Service</oppia-noninteractive-policy> and <oppia-noninteractive-policy link="privacy">Privacy Policy</oppia-noninteractive-policy>.
+ Please visit <a href="https://www.oppia.org/terms">this page</a> for the latest version of these terms.
+ What is %s?
+ Who is an Administrator?
+ How can I create a new profile?
+ How do I get the app in my language?
+ I found a bug. How can I report it?
+ Why are there only math lessons?
+ Will you be making more lessons?
+ Why is the Exploration player not loading?
+ Why is my audio not playing?
+ How can I delete a profile?
+ How do I update the app?
+ How do I update my Android OS?
+ I can\'t find my question here. What now?
- What is %s?
- Who is an Administrator?
- How can I create a new profile?
- How do I get the app in my language?
- I found a bug. How can I report it?
- Why are there only math lessons?
- Will you be making more lessons?
- Why is the Exploration player not loading?
- Why is my audio not playing?
- How can I delete a profile?
- How do I update the app?
- How do I update my Android OS?
- I can\'t find my question here. What now?
-
- %1$s "O-pee-yah" (Finnish) - "to learn"
%1$s\'s mission is to help anyone learn anything they want in an effective and enjoyable way.
By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, %1$s aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.
As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!
]]>
- An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you.
Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as changing your PIN, and more.
To see who your Administrator is, go to the Profile Chooser. The first profile listed and has "Administrator" written under their name is the Administrator.
]]>
- If it is your first time creating a profile and you do not have a PIN:From the Profile Chooser, tap on Set up Multiple Profiles . Create a PIN and Save . Fill in all fields for the profile.(Optional) Upload a photo. Enter a name. (Optional) Assign a 3-digit PIN. Tap Create . This profile is added to your Profile Chooser! If you have created a profile before and have a PIN:
From the Profile Chooser, tap on Add Profile . Enter your PIN and tap Submit . Fill in all fields for the profile. (Optional) Upload a photo. Enter a name. (Optional) Assign a 3-digit PIN. Tap Create . This profile is added to your Profile Chooser! Note: Only the Administrator is able to manage profiles.
]]>
- The %s app currently supports English, Brazilian Portuguese, Arabic, Swahili and Nigerian Pidgin. Choose one of these languages in the menu, under Options. To request the app in your language, please contact us at admin@oppia.org.]]>
- From your %s app home screen, tap the menu in the top left corner. Tap Share feedback . Follow the instructions to report the bug or share feedback. ]]>
- %1$s’s mission is to help learners gain necessary life skills. Math is an essential skill in everyday life. %1$s will be offering new lessons on science and other subjects soon!]]>
- Yes, %s will be offering new lessons on science and other subjects soon. Please check back for updates!]]>
- If the Exploration Player is not loading
Check to see if the app is up to date:
Go to the Play Store and make sure the app is updated to its latest version
Check your internet connection:
If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Ask the Administrator to check their device and internet connection:
Get the Administrator to troubleshoot using the steps above Let us know if you still have issues with loading:
Report a problem by contacting us at admin@oppia.org. ]]>
- If your audio is not playing
Check to see if the app is up to date:
Go to the Play Store and make sure the app is updated to its latest version
Check your internet connection:
If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play.
Ask the Administrator to check their device and internet connection:
Get the Administrator to troubleshoot using the steps above
Let us know if you still have issues with loading:
Report a problem by contacting us at admin@oppia.org. ]]>
- Once a profile is deleted:The profile cannot be recovered. Profile information such as name, photos, and progress will be permanently deleted. To delete a profile (excluding the Administrator\'s ):
From the Administrator\'s Home Page, tap on the menu button on the top left. Tap on Administrator Controls . Tap on Edit Profiles . Tap on the Profile you would like to delete. At the bottom of the screen, tap Profile Deletion . Tap Delete to confirm deletion.
Note: Only the Administrator is able to manage profiles.
]]>
- Open the Google Play Store app. Search for the %s app. Tap Update.]]>
- Tap your phone\'s Settings app. Tap System updates. Tap System updates and follow the instructions to update your Android operating system.]]>
- If you cannot find your question or would like to report a bug, contact us at admin@oppia.org. ]]>
+ <p>%1$s <i>"O-pee-yah"</i> (Finnish) - "to learn"</p><p><br></p><p>%1$s\'s mission is to help anyone learn anything they want in an effective and enjoyable way.</p><p><br></p><p>By creating a set of free, high-quality, demonstrably effective lessons with the help of educators from around the world, %1$s aims to provide students with quality education — regardless of where they are or what traditional resources they have access to.</p><p><br></p><p>As a student, you can begin your learning adventure by browsing the topics listed on the Home Page!</p>
+ <p>An Administrator is the main user that manages profiles and settings for every profile on their account. They are most likely your parent, teacher, or guardian that created this profile for you. </p><p><br></p><p>Administrators have the ability to manage profiles, assign PINs, and change other settings under their account. Depending on your profile, Administrator permissions may be required for certain features such as changing your PIN, and more. </p><p><br></p><p>To see who your Administrator is, go to the Profile Chooser. The first profile listed and has "Administrator" written under their name is the Administrator. </p>
+ <p>If it is your first time creating a profile and you do not have a PIN:<ol><li>From the Profile Chooser, tap on <strong>Set up Multiple Profiles</strong>.</li><li>Create a PIN and <strong>Save</strong>.</li><li>Fill in all fields for the profile.<ol><li>(Optional) Upload a photo.</li><li>Enter a name.</li><li>(Optional) Assign a 3-digit PIN.</li></ol></li><li>Tap <strong>Create</strong>. This profile is added to your Profile Chooser!</li></ol></p><p> If you have created a profile before and have a PIN:<ol><li>From the Profile Chooser, tap on <strong>Add Profile</strong>. </li><li>Enter your PIN and tap <strong>Submit</strong>. </li><li>Fill in all fields for the profile.<ol><li> (Optional) Upload a photo. </li><li> Enter a name. </li><li> (Optional) Assign a 3-digit PIN. </li></ol></li><li>Tap <strong>Create</strong>. This profile is added to your Profile Chooser!</li></ol></p><br><p>Note: Only the <u>Administrator</u> is able to manage profiles.</p>
+ <p>The %s app currently supports English, Brazilian Portuguese, Arabic, Swahili and Nigerian Pidgin. Choose one of these languages in the menu, under Options. To request the app in your language, please contact us at <strong>admin@oppia.org<strong>.</p>
+ <p><ol><li>From your %s app home screen, tap the menu in the top left corner.</li><li>Tap <strong>Share feedback</strong>.</li><li>Follow the instructions to report the bug or share feedback.</li></p>
+ <p>%1$s\'s mission is to help learners gain necessary life skills. Math is an essential skill in everyday life. %1$s will be offering new lessons on science and other subjects soon!</p>
+ <p>Yes, %s will be offering new lessons on science and other subjects soon. Please check back for updates!</p>
+ <p>If the Exploration Player is not loading</p><p><br></p><p>Check to see if the app is up to date:</p><p><ul><li> Go to the Play Store and make sure the app is updated to its latest version </li></ul><p><br></p><p>Check your internet connection:</p><ul><li> If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. </li></ul><p>Ask the Administrator to check their device and internet connection:</p><ul><li> Get the Administrator to troubleshoot using the steps above </li></ul><p>Let us know if you still have issues with loading:</p><ul><li> Report a problem by contacting us at admin@oppia.org. </li></ul>
+ <p>If your audio is not playing</p><p><br></p><p>Check to see if the app is up to date:</p><ul><li> Go to the Play Store and make sure the app is updated to its latest version </li></ul><p><br></p><p>Check your internet connection:</p><ul><li> If your internet connection is slow, try re-connecting to your Wi-Fi network or connecting to a different network. Slow internet may cause the audio to load irregularly, making it difficult to play. </li></ul><p><br></p><p>Ask the Administrator to check their device and internet connection:</p><ul><li> Get the Administrator to troubleshoot using the steps above</li></ul><p><br></p><p>Let us know if you still have issues with loading:</p><ul><li> Report a problem by contacting us at admin@oppia.org. </li></ul>
+ <p>Once a profile is deleted:</p><ol><li>The profile cannot be recovered. </li><li> Profile information such as name, photos, and progress will be permanently deleted. </li></ol><p>To delete a profile (excluding the <u>Administrator\'s</u>):</p><ol><li> From the Administrator\'s Home Page, tap on the menu button on the top left. </li><li>Tap on <strong>Administrator Controls</strong>. </li><li>Tap on <strong>Edit Profiles</strong>. </li><li>Tap on the Profile you would like to delete. </li><li>At the bottom of the screen, tap <strong>Profile Deletion</strong>. </li><li>Tap <strong>Delete</strong> to confirm deletion. </li></ol><p><br></p><p>Note: Only the <u>Administrator</u> is able to manage profiles.</p>
+ <p><ol><li>Open the Google Play Store app.</li><li>Search for the %s app.</li><li>Tap Update.</p>
+ <p><ol><li>Tap your phone\'s Settings app.</li><li>Tap System updates.</li><li>Tap System updates and follow the instructions to update your Android operating system.</p>
+ <p>If you cannot find your question or would like to report a bug, contact us at <strong>admin@oppia.org.</strong></p>
Profile Edit Fragment Test Activity
Administrator Controls Fragment Test Activity
@@ -616,7 +620,6 @@
On a scale from 0–10, how likely are you to recommend %s to a friend or colleague?
The previous subtopic is %s
The next subtopic is %s
-
App Info
Spotlight Overlay Arrow
Close Spotlight Button
diff --git a/app/src/sharedTest/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsViewTest.kt
index 2b8ea2dc02d..606db46927f 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsViewTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsViewTest.kt
@@ -7,9 +7,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withHint
import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat
import dagger.Component
@@ -31,9 +33,9 @@ import org.oppia.android.app.application.testing.TestingBuildFlavorModule
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.model.CustomSchemaValue
-import org.oppia.android.app.model.InputInteractionViewTestActivityParams
-import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType
import org.oppia.android.app.model.Interaction
+import org.oppia.android.app.model.MathExpressionInteractionsViewTestActivityParams
+import org.oppia.android.app.model.MathExpressionInteractionsViewTestActivityParams.MathInteractionType
import org.oppia.android.app.model.OppiaLanguage
import org.oppia.android.app.model.SchemaObject
import org.oppia.android.app.model.SchemaObjectList
@@ -44,7 +46,7 @@ import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory.REA
import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory.SUBMIT_TIME
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
-import org.oppia.android.app.testing.InputInteractionViewTestActivity
+import org.oppia.android.app.testing.MathExpressionInteractionsViewTestActivity
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
import org.oppia.android.data.backends.gae.NetworkModule
@@ -257,6 +259,63 @@ class MathExpressionInteractionsViewTest {
}
}
+ @Test
+ fun testNumericExpression_submitWithBlankInput_emptyInputErrorIsDisplayed() {
+ val interaction = createInteractionWithPlaceholder("test placeholder")
+ launchForNumericExpressions(interaction).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity -> activity.getInteractionView().requestFocus() }
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.submit_button)).perform(click())
+ onView(withId(R.id.math_expression_input_error))
+ .check(
+ matches(
+ withText(
+ R.string.numeric_expression_error_empty_input
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testAlgebraicExpression_submitWithBlankInput_emptyInputErrorIsDisplayed() {
+ val interaction = createInteractionWithPlaceholder("test placeholder")
+ launchForAlgebraicExpressions(interaction).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity -> activity.getInteractionView().requestFocus() }
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.submit_button)).perform(click())
+ onView(withId(R.id.math_expression_input_error))
+ .check(
+ matches(
+ withText(
+ R.string.algebraic_expression_error_empty_input
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testMathEquation_submitWithBlankInput_emptyInputErrorIsDisplayed() {
+ val interaction = createInteractionWithPlaceholder("test placeholder")
+ launchForMathEquations(interaction).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity -> activity.getInteractionView().requestFocus() }
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.submit_button)).perform(click())
+ onView(withId(R.id.math_expression_input_error))
+ .check(
+ matches(
+ withText(
+ R.string.math_equation_error_empty_input
+ )
+ )
+ )
+ }
+ }
+
@Test
fun testView_gainFocus_resetsTypeFace() {
launchForNumericExpressions().use { scenario ->
@@ -1602,6 +1661,28 @@ class MathExpressionInteractionsViewTest {
}
}
+ @Test
+ @RunParameterized(
+ Iteration("numeric_expression", "type=NUMERIC_EXPRESSION", "text="),
+ Iteration("algebraic_expression", "type=ALGEBRAIC_EXPRESSION", "text="),
+ Iteration("math_equation", "type=MATH_EQUATION", "text=")
+ )
+ fun testView_allInteractions_blankInput_produceSubmitTimeError() {
+ val interactionType = MathInteractionType.valueOf(type)
+ val interaction = createInteraction()
+ launch(interactionType, interaction).use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+
+ typeExpressionInput(text)
+
+ // Using not-allowed-listed variables should result in a failure.
+ scenario.onActivity { activity ->
+ val answerError = activity.mathExpressionViewModel.checkPendingAnswerError(SUBMIT_TIME)
+ assertThat(answerError).isNotEmpty()
+ }
+ }
+ }
+
@Test
@RunParameterized(
Iteration("numeric_expression_valid", "type=NUMERIC_EXPRESSION", "text=0/1"),
@@ -1630,26 +1711,33 @@ class MathExpressionInteractionsViewTest {
private fun launchForNumericExpressions(
interaction: Interaction = Interaction.getDefaultInstance(),
translationContext: WrittenTranslationContext = WrittenTranslationContext.getDefaultInstance()
- ): ActivityScenario {
+ ): ActivityScenario {
return launch(MathInteractionType.NUMERIC_EXPRESSION, interaction, translationContext)
}
private fun launchForMathEquations(
interaction: Interaction = Interaction.getDefaultInstance(),
translationContext: WrittenTranslationContext = WrittenTranslationContext.getDefaultInstance()
- ): ActivityScenario {
+ ): ActivityScenario {
return launch(MathInteractionType.MATH_EQUATION, interaction, translationContext)
}
+ private fun launchForAlgebraicExpressions(
+ interaction: Interaction = Interaction.getDefaultInstance(),
+ translationContext: WrittenTranslationContext = WrittenTranslationContext.getDefaultInstance()
+ ): ActivityScenario {
+ return launch(MathInteractionType.ALGEBRAIC_EXPRESSION, interaction, translationContext)
+ }
+
private fun launch(
interactionType: MathInteractionType,
interaction: Interaction = Interaction.getDefaultInstance(),
translationContext: WrittenTranslationContext = WrittenTranslationContext.getDefaultInstance()
- ): ActivityScenario {
+ ): ActivityScenario {
return ActivityScenario.launch(
- InputInteractionViewTestActivity.createIntent(
+ MathExpressionInteractionsViewTestActivity.createIntent(
ApplicationProvider.getApplicationContext(),
- InputInteractionViewTestActivityParams.newBuilder().apply {
+ MathExpressionInteractionsViewTestActivityParams.newBuilder().apply {
this.interaction = interaction
writtenTranslationContext = translationContext
mathInteractionType = interactionType
@@ -1746,7 +1834,7 @@ class MathExpressionInteractionsViewTest {
}.build()
}
- private fun InputInteractionViewTestActivity.getInteractionView(): TextView =
+ private fun MathExpressionInteractionsViewTestActivity.getInteractionView(): TextView =
findViewById(R.id.test_math_expression_input_interaction_view)
private fun setUpTestApplicationComponent() {
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 48da829d39e..ab857016665 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
@@ -154,11 +154,11 @@ class FAQListFragmentTest {
allOf(
hasExtra(
FAQSingleActivity.FAQ_SINGLE_ACTIVITY_QUESTION,
- getResources().getString(R.string.faq_question_3)
+ getResources().getString(R.string.faq_question_create_profile)
),
hasExtra(
FAQSingleActivity.FAQ_SINGLE_ACTIVITY_ANSWER,
- getResources().getString(R.string.faq_answer_3)
+ getResources().getString(R.string.faq_answer_create_profile)
),
hasComponent(FAQSingleActivity::class.java.name)
)
@@ -180,11 +180,11 @@ class FAQListFragmentTest {
allOf(
hasExtra(
FAQSingleActivity.FAQ_SINGLE_ACTIVITY_QUESTION,
- getResources().getString(R.string.faq_question_3)
+ getResources().getString(R.string.faq_question_create_profile)
),
hasExtra(
FAQSingleActivity.FAQ_SINGLE_ACTIVITY_ANSWER,
- getResources().getString(R.string.faq_answer_3)
+ getResources().getString(R.string.faq_answer_create_profile)
),
hasComponent(FAQSingleActivity::class.java.name)
)
@@ -205,11 +205,11 @@ class FAQListFragmentTest {
allOf(
hasExtra(
FAQSingleActivity.FAQ_SINGLE_ACTIVITY_QUESTION,
- getResources().getString(R.string.faq_question_1, getAppName())
+ getResources().getString(R.string.faq_question_whats_oppia, getAppName())
),
hasExtra(
FAQSingleActivity.FAQ_SINGLE_ACTIVITY_ANSWER,
- getResources().getString(R.string.faq_answer_1, getAppName())
+ getResources().getString(R.string.faq_answer_whats_oppia, getAppName())
),
hasComponent(FAQSingleActivity::class.java.name)
)
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 9bfd477a36b..c3f85f719d8 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
@@ -191,7 +191,7 @@ class FAQSingleActivityTest {
displayLocale = appLanguageLocaleHandler.getDisplayLocale()
)
val htmlResult: Spannable = htmlParser.parseOppiaHtml(
- getResources().getString(R.string.faq_answer_1),
+ getResources().getString(R.string.faq_answer_whats_oppia),
answerTextView
)
assertThat(answerTextView.text.toString()).isEqualTo(htmlResult.toString())
@@ -204,8 +204,8 @@ class FAQSingleActivityTest {
private fun createFAQSingleActivity(): Intent {
return FAQSingleActivity.createFAQSingleActivityIntent(
ApplicationProvider.getApplicationContext(),
- getResources().getString(R.string.faq_question_1),
- getResources().getString(R.string.faq_answer_1)
+ getResources().getString(R.string.faq_question_whats_oppia),
+ getResources().getString(R.string.faq_answer_whats_oppia)
)
}
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 571ba12ee85..1ac4f3d429b 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
@@ -852,8 +852,10 @@ class HtmlParserTest {
}
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun ActivityScenario .getDimensionPixelSize(
@DimenRes dimenResId: Int
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 24193e64a33..a05e4756684 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
@@ -747,6 +747,96 @@ class StateFragmentTest {
}
}
+ @Test
+ fun testStateFragment_loadDragDropExp_submitWithoutArranging_showsErrorMessage() {
+ setUpTestWithLanguageSwitchingFeatureOff()
+ launchForExploration(TEST_EXPLORATION_ID_4, shouldSavePartialProgress = false).use {
+ startPlayingExploration()
+ clickSubmitAnswerButton()
+ onView(withId(R.id.drag_drop_interaction_error))
+ .check(
+ matches(
+ withText(
+ R.string.drag_and_drop_interaction_empty_input
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testStateFragment_loadDragDropExp_withGrouping_submitWithoutArranging_showsErrorMessage_dragItem_errorMessageIsReset() { // ktlint-disable max-line-length
+ setUpTestWithLanguageSwitchingFeatureOff()
+ launchForExploration(TEST_EXPLORATION_ID_4, shouldSavePartialProgress = false).use {
+ startPlayingExploration()
+
+ // Drag and drop interaction with grouping.
+ // Submit answer without any changes.
+ clickSubmitAnswerButton()
+ // Empty input error is displayed.
+ onView(withId(R.id.drag_drop_interaction_error))
+ .check(
+ matches(
+ isDisplayed()
+ )
+ )
+ // Submit button is disabled due to the error.
+ verifySubmitAnswerButtonIsDisabled()
+ // Drag and rearrange an item.
+ dragAndDropItem(fromPosition = 0, toPosition = 1)
+ // Empty input error is reset.
+ onView(withId(R.id.drag_drop_interaction_error))
+ .check(
+ matches(
+ not(isDisplayed())
+ )
+ )
+ // Submit button is enabled back.
+ verifySubmitAnswerButtonIsEnabled()
+ }
+ }
+
+ @Test
+ fun testStateFragment_loadDragDropExp_withoutGrouping_submitWithoutArranging_showsErrorMessage_dragItem_errorMessageIsReset() { // ktlint-disable max-line-length
+ setUpTestWithLanguageSwitchingFeatureOff()
+ launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use {
+ startPlayingExploration()
+ playThroughPrototypeState1()
+ playThroughPrototypeState2()
+ playThroughPrototypeState3()
+ playThroughPrototypeState4()
+ playThroughPrototypeState5()
+ playThroughPrototypeState6()
+ playThroughPrototypeState7()
+ playThroughPrototypeState8()
+
+ // Drag and drop interaction without grouping.
+ // Ninth state: Drag Drop Sort. Correct answer: Move 1st item to 4th position.
+ // Submit answer without any changes.
+ clickSubmitAnswerButton()
+ // Empty input error is displayed.
+ onView(withId(R.id.drag_drop_interaction_error))
+ .check(
+ matches(
+ isDisplayed()
+ )
+ )
+ // Submit button is disabled due to the error.
+ verifySubmitAnswerButtonIsDisabled()
+ // Drag and rearrange an item.
+ dragAndDropItem(fromPosition = 0, toPosition = 1)
+ // Empty input error is reset.
+ onView(withId(R.id.drag_drop_interaction_error))
+ .check(
+ matches(
+ not(isDisplayed())
+ )
+ )
+ // Submit button is enabled back.
+ verifySubmitAnswerButtonIsEnabled()
+ }
+ }
+
@Test
fun testStateFragment_loadDragDropExp_mergeFirstTwoItems_worksCorrectly() {
setUpTestWithLanguageSwitchingFeatureOff()
@@ -2738,7 +2828,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(NUMERIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("no reordering allowed")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -2754,7 +2844,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(NUMERIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("no reordering allowed")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -2770,7 +2860,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(NUMERIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("no reordering allowed")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -2822,7 +2912,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(NUMERIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("commutative and associative")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -2839,7 +2929,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(NUMERIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("commutative and associative")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -2909,7 +2999,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(NUMERIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("any equivalent expression")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3076,7 +3166,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(ALGEBRAIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("represents the product of")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3093,7 +3183,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(ALGEBRAIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("represents the product of")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3110,7 +3200,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(ALGEBRAIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("represents the product of")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3162,7 +3252,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(ALGEBRAIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("commutative and associative")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3179,7 +3269,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(ALGEBRAIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("commutative and associative")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3250,7 +3340,7 @@ class StateFragmentTest {
// values being different.
verifyViewTypeIsPresent(ALGEBRAIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("any equivalent expression")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3286,7 +3376,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(ALGEBRAIC_EXPRESSION_INPUT_INTERACTION)
verifyContentContains("any equivalent expression")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3467,7 +3557,7 @@ class StateFragmentTest {
// matters for this interaction).
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("algebraic equation represents the quantity")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3484,7 +3574,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("algebraic equation represents the quantity")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3501,7 +3591,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("algebraic equation represents the quantity")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3518,7 +3608,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("algebraic equation represents the quantity")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3553,7 +3643,7 @@ class StateFragmentTest {
// matters for this interaction).
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("commutative and associative")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3588,7 +3678,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("commutative and associative")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3605,7 +3695,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("commutative and associative")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3702,7 +3792,7 @@ class StateFragmentTest {
// values being different.
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("any equivalent expression")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3761,7 +3851,7 @@ class StateFragmentTest {
// verify that the two sides are multiples of each other).
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("any equivalent expression")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
@@ -3778,7 +3868,7 @@ class StateFragmentTest {
// Verify that the state hasn't changed since the answer is incorrect.
verifyViewTypeIsPresent(MATH_EQUATION_INPUT_INTERACTION)
verifyContentContains("any equivalent expression")
- verifySubmitAnswerButtonIsDisabled()
+ verifySubmitAnswerButtonIsEnabled() // Wrong answers shouldn't disable submit.
}
}
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 2ce962ee356..5b6cdd85319 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
@@ -420,60 +420,6 @@ class InputInteractionViewTestActivityTest {
)
}
- @Test
- fun testTextInput_withNoInput_hasCorrectPendingAnswerType() {
- val activityScenario = ActivityScenario.launch(
- InputInteractionViewTestActivity::class.java
- )
- activityScenario.onActivity { activity ->
- val pendingAnswer = activity.textInputViewModel.getPendingAnswer()
- assertThat(pendingAnswer.answer).isInstanceOf(InteractionObject::class.java)
- assertThat(pendingAnswer.answer.normalizedString).isEmpty()
- }
- }
-
- @Test
- @DisableAccessibilityChecks // Disabled, as InputInteractionViewTestActivity is a test file and
- // will not be used by user
- fun testTextInput_withChar_hasCorrectPendingAnswer() {
- val activityScenario = ActivityScenario.launch(
- InputInteractionViewTestActivity::class.java
- )
- onView(withId(R.id.test_text_input_interaction_view))
- .perform(
- editTextInputAction.appendText(
- "abc"
- )
- )
- activityScenario.onActivity { activity ->
- val pendingAnswer = activity.textInputViewModel.getPendingAnswer()
- assertThat(pendingAnswer.answer).isInstanceOf(InteractionObject::class.java)
- assertThat(pendingAnswer.answer.objectTypeCase).isEqualTo(
- InteractionObject.ObjectTypeCase.NORMALIZED_STRING
- )
- assertThat(pendingAnswer.answer.normalizedString).isEqualTo("abc")
- }
- }
-
- @Test
- @Ignore("Landscape not properly supported") // TODO(#56): Reenable once landscape is supported.
- fun testTextInput_withChar_configChange_hasCorrectPendingAnswer() {
- val activityScenario = ActivityScenario.launch(
- InputInteractionViewTestActivity::class.java
- )
- onView(withId(R.id.test_text_input_interaction_view))
- .perform(
- editTextInputAction.appendText(
- "abc"
- )
- )
- activityScenario.onActivity { activity ->
- activity.requestedOrientation = Configuration.ORIENTATION_LANDSCAPE
- }
- onView(withId(R.id.test_text_input_interaction_view)).check(matches(isDisplayed()))
- .check(matches(withText("abc")))
- }
-
private fun scrollToSubmitButton() {
onView(withId(R.id.submit_button)).perform(scrollTo())
testCoroutineDispatchers.runCurrent()
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivityTest.kt
new file mode 100644
index 00000000000..eb6af85cbb6
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivityTest.kt
@@ -0,0 +1,276 @@
+package org.oppia.android.app.testing
+
+import android.app.Application
+import android.content.res.Configuration
+import androidx.appcompat.app.AppCompatActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.scrollTo
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.R
+import org.oppia.android.app.activity.ActivityComponent
+import org.oppia.android.app.activity.ActivityComponentFactory
+import org.oppia.android.app.activity.route.ActivityRouterModule
+import org.oppia.android.app.application.ApplicationComponent
+import org.oppia.android.app.application.ApplicationInjector
+import org.oppia.android.app.application.ApplicationInjectorProvider
+import org.oppia.android.app.application.ApplicationModule
+import org.oppia.android.app.application.ApplicationStartupListenerModule
+import org.oppia.android.app.application.testing.TestingBuildFlavorModule
+import org.oppia.android.app.devoptions.DeveloperOptionsModule
+import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.model.InteractionObject
+import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.data.backends.gae.NetworkConfigProdModule
+import org.oppia.android.data.backends.gae.NetworkModule
+import org.oppia.android.domain.classify.InteractionsModule
+import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule
+import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule
+import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule
+import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.android.domain.exploration.ExplorationProgressModule
+import org.oppia.android.domain.exploration.ExplorationStorageModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule
+import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.domain.oppialogger.LoggingIdentifierModule
+import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule
+import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule
+import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule
+import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
+import org.oppia.android.domain.platformparameter.PlatformParameterModule
+import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
+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.testing.DisableAccessibilityChecks
+import org.oppia.android.testing.OppiaTestRule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.espresso.EditTextInputAction
+import org.oppia.android.testing.firebase.TestAuthenticationModule
+import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
+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.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.locale.LocaleProdModule
+import org.oppia.android.util.logging.EventLoggingConfigurationModule
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.logging.SyncStatusModule
+import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
+import org.oppia.android.util.parser.image.GlideImageLoaderModule
+import org.oppia.android.util.parser.image.ImageParsingModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [TextInputInteractionViewTestActivity]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = TextInputInteractionViewTestActivityTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class TextInputInteractionViewTestActivityTest {
+ @get:Rule
+ val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
+
+ @Inject
+ lateinit var editTextInputAction: EditTextInputAction
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ @Test
+ @DisableAccessibilityChecks // Disabled, as TextInputInteractionViewTestActivity is a test file and
+ // will not be used by user
+ fun testTextInput_withNoInput_hasCorrectPendingAnswerType() {
+ val activityScenario = ActivityScenario.launch(
+ TextInputInteractionViewTestActivity::class.java
+ )
+ activityScenario.onActivity { activity ->
+ val pendingAnswer = activity.textInputViewModel.getPendingAnswer()
+ assertThat(pendingAnswer.answer).isInstanceOf(InteractionObject::class.java)
+ assertThat(pendingAnswer.answer.normalizedString).isEmpty()
+ }
+ }
+
+ @Test
+ @DisableAccessibilityChecks // Disabled, as TextInputInteractionViewTestActivity is a test file and
+ // will not be used by user
+ fun testTextInput_withChar_hasCorrectPendingAnswer() {
+ val activityScenario = ActivityScenario.launch(
+ TextInputInteractionViewTestActivity::class.java
+ )
+ onView(withId(R.id.test_text_input_interaction_view))
+ .perform(
+ editTextInputAction.appendText(
+ "abc"
+ )
+ )
+ activityScenario.onActivity { activity ->
+ val pendingAnswer = activity.textInputViewModel.getPendingAnswer()
+ assertThat(pendingAnswer.answer).isInstanceOf(InteractionObject::class.java)
+ assertThat(pendingAnswer.answer.objectTypeCase).isEqualTo(
+ InteractionObject.ObjectTypeCase.NORMALIZED_STRING
+ )
+ assertThat(pendingAnswer.answer.normalizedString).isEqualTo("abc")
+ }
+ }
+
+ @Test
+ @Ignore("Landscape not properly supported") // TODO(#56): Reenable once landscape is supported.
+ fun testTextInput_withChar_configChange_hasCorrectPendingAnswer() {
+ val activityScenario = ActivityScenario.launch(
+ TextInputInteractionViewTestActivity::class.java
+ )
+ onView(withId(R.id.test_text_input_interaction_view))
+ .perform(
+ editTextInputAction.appendText(
+ "abc"
+ )
+ )
+ activityScenario.onActivity { activity ->
+ activity.requestedOrientation = Configuration.ORIENTATION_LANDSCAPE
+ }
+ onView(withId(R.id.test_text_input_interaction_view))
+ .check(matches(isDisplayed()))
+ .check(
+ matches(withText("abc"))
+ )
+ }
+
+ @Test
+ @DisableAccessibilityChecks // Disabled, as TextInputInteractionViewTestActivity is a test file and
+ // will not be used by user
+ fun testTextInput_withBlankInput_submit_emptyInputErrorIsDisplayed() {
+ ActivityScenario.launch(TextInputInteractionViewTestActivity::class.java).use {
+ scrollToSubmitButton()
+ onView(withId(R.id.submit_button)).check(matches(isDisplayed()))
+ .perform(
+ click()
+ )
+ onView(withId(R.id.text_input_error))
+ .check(
+ matches(
+ withText(
+ R.string.text_error_empty_input
+ )
+ )
+ )
+ }
+ }
+
+ private fun scrollToSubmitButton() {
+ onView(withId(R.id.submit_button)).perform(scrollTo())
+ testCoroutineDispatchers.runCurrent()
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ @Singleton
+ @Component(
+ modules = [
+ RobolectricModule::class, TestAuthenticationModule::class,
+ PlatformParameterModule::class, PlatformParameterSingletonModule::class,
+ TestDispatcherModule::class, ApplicationModule::class,
+ LoggerModule::class, ContinueModule::class, FractionInputModule::class,
+ ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
+ NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
+ DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class,
+ GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class,
+ ApplicationStartupListenerModule::class, LogReportWorkerModule::class,
+ HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class,
+ FirebaseLogUploaderModule::class, FakeOppiaClockModule::class,
+ DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
+ ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class,
+ NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class,
+ MathEquationInputModule::class, SplitScreenInteractionModule::class,
+ LoggingIdentifierModule::class, ApplicationLifecycleModule::class,
+ SyncStatusModule::class, MetricLogSchedulerModule::class, TestingBuildFlavorModule::class,
+ EventLoggingConfigurationModule::class, ActivityRouterModule::class,
+ CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(inputInteractionViewTestActivityTest: TextInputInteractionViewTestActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerTextInputInteractionViewTestActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(inputInteractionViewTestActivityTest: TextInputInteractionViewTestActivityTest) {
+ component.inject(inputInteractionViewTestActivityTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
+}
diff --git a/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt b/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt
index d993ffd3de8..a7904d0d1cf 100644
--- a/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt
+++ b/app/src/test/java/org/oppia/android/app/parser/ListItemLeadingMarginSpanTest.kt
@@ -1044,8 +1044,10 @@ class ListItemLeadingMarginSpanTest {
return valueCaptor.value
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
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
index 51be2481321..0eeb999c8fe 100644
--- a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt
@@ -62,7 +62,8 @@ class LocaleController @Inject constructor(
private val asyncDataSubscriptionManager: AsyncDataSubscriptionManager,
private val machineLocale: MachineLocale,
private val androidLocaleFactory: AndroidLocaleFactory,
- private val formatterFactory: OppiaBidiFormatter.Factory
+ private val formatterFactory: OppiaBidiFormatter.Factory,
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
) {
private val definitionsLock = ReentrantLock()
private lateinit var supportedLanguages: SupportedLanguages
@@ -115,9 +116,8 @@ class LocaleController @Inject constructor(
* for cases in which the user changed their selected language).
*/
fun reconstituteDisplayLocale(oppiaLocaleContext: OppiaLocaleContext): DisplayLocale {
- return DisplayLocaleImpl(
- oppiaLocaleContext, machineLocale, androidLocaleFactory, formatterFactory
- )
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(oppiaLocaleContext)
+ return DisplayLocaleImpl(oppiaLocaleContext, formattingLocale, machineLocale, formatterFactory)
}
/**
@@ -257,7 +257,7 @@ class LocaleController @Inject constructor(
private fun getSystemLocaleProfile(): DataProvider {
return dataProviders.createInMemoryDataProvider(ANDROID_SYSTEM_LOCALE_DATA_PROVIDER_ID) {
- AndroidLocaleProfile.createFrom(getSystemLocale())
+ androidLocaleProfileFactory.createFrom(getSystemLocale())
}
}
@@ -293,7 +293,7 @@ class LocaleController @Inject constructor(
@Suppress("DEPRECATION") // Old API is needed for SDK versions < N.
private fun getDefaultLocale(configuration: Configuration): Locale = configuration.locale
- private suspend fun computeLocaleResult(
+ private suspend inline fun computeLocaleResult(
language: OppiaLanguage,
systemLocaleProfile: AndroidLocaleProfile,
usageMode: LanguageUsageMode
@@ -302,7 +302,6 @@ class LocaleController @Inject constructor(
// 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)
@@ -324,7 +323,13 @@ class LocaleController @Inject constructor(
retrieveLanguageDefinition(languageDefinition.fallbackMacroLanguage)?.let {
fallbackLanguageDefinition = it
}
- regionDefinition = retrieveRegionDefinition(systemLocaleProfile.regionCode)
+ val regionCode = when (systemLocaleProfile) {
+ is AndroidLocaleProfile.LanguageAndRegionProfile -> systemLocaleProfile.regionCode
+ is AndroidLocaleProfile.RegionOnlyProfile -> systemLocaleProfile.regionCode
+ is AndroidLocaleProfile.LanguageAndWildcardRegionProfile,
+ is AndroidLocaleProfile.LanguageOnlyProfile, is AndroidLocaleProfile.RootProfile -> ""
+ }
+ regionDefinition = retrieveRegionDefinition(regionCode)
this.usageMode = usageMode
}.build()
@@ -343,8 +348,10 @@ class LocaleController @Inject constructor(
}
return when (usageMode) {
- APP_STRINGS ->
- DisplayLocaleImpl(localeContext, machineLocale, androidLocaleFactory, formatterFactory)
+ APP_STRINGS -> {
+ val formattingLocale = androidLocaleFactory.createAndroidLocaleAsync(localeContext).await()
+ DisplayLocaleImpl(localeContext, formattingLocale, machineLocale, formatterFactory)
+ }
CONTENT_STRINGS, AUDIO_TRANSLATIONS -> ContentLocaleImpl(localeContext)
USAGE_MODE_UNSPECIFIED, UNRECOGNIZED -> null
}
@@ -413,9 +420,7 @@ class LocaleController @Inject constructor(
return@mapNotNull definition.retrieveAppLanguageProfile()?.let { profile ->
profile to definition
}
- }.find { (profile, _) ->
- localeProfile.matches(machineLocale, profile)
- }?.let { (_, definition) -> definition }
+ }.find { (profile, _) -> localeProfile.matches(profile) }?.let { (_, definition) -> definition }
}
private suspend fun retrieveRegionDefinition(countryCode: String): RegionSupportDefinition {
@@ -431,7 +436,7 @@ class LocaleController @Inject constructor(
} ?: RegionSupportDefinition.newBuilder().apply {
region = OppiaRegion.REGION_UNSPECIFIED
regionId = RegionSupportDefinition.IetfBcp47RegionId.newBuilder().apply {
- ietfRegionTag = countryCode
+ ietfRegionTag = machineLocale.run { countryCode.toMachineUpperCase() }
}.build()
}.build()
}
@@ -455,10 +460,10 @@ class LocaleController @Inject constructor(
private fun LanguageSupportDefinition.retrieveAppLanguageProfile(): AndroidLocaleProfile? {
return when (appStringId.languageTypeCase) {
LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID ->
- AndroidLocaleProfile.createFromIetfDefinitions(appStringId, regionDefinition = null)
+ androidLocaleProfileFactory.createFromIetfDefinitions(appStringId, regionDefinition = null)
LanguageSupportDefinition.LanguageId.LanguageTypeCase.MACARONIC_ID -> {
// Likely won't match against system languages.
- AndroidLocaleProfile.createFromMacaronicLanguage(appStringId)
+ androidLocaleProfileFactory.createFromMacaronicLanguage(appStringId)
}
LanguageSupportDefinition.LanguageId.LanguageTypeCase.LANGUAGETYPE_NOT_SET, null -> null
}
@@ -473,9 +478,12 @@ class LocaleController @Inject constructor(
// 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 = systemLocaleProfile.computeIetfLanguageTag()
- }.build()
+ // The root profile is assumed if there is no specific language ID to use.
+ if (systemLocaleProfile !is AndroidLocaleProfile.RootProfile) {
+ ietfBcp47Id = LanguageSupportDefinition.IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = systemLocaleProfile.ietfLanguageTag
+ }.build()
+ }
}.build()
}.build()
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
index 30467ee6263..9a5eb36e8cc 100644
--- a/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/locale/LocaleControllerTest.kt
@@ -114,12 +114,16 @@ class LocaleControllerTest {
}
@Test
- fun testReconstituteDisplayLocale_defaultContext_returnsDisplayLocaleForContext() {
+ fun testReconstituteDisplayLocale_defaultContext_throwsException() {
val context = OppiaLocaleContext.getDefaultInstance()
- val locale = localeController.reconstituteDisplayLocale(context)
+ val exception = assertThrows() {
+ localeController.reconstituteDisplayLocale(context)
+ }
- assertThat(locale.localeContext).isEqualToDefaultInstance()
+ // A default locale context isn't valid by itself (though it can represent the root locale when
+ // at least the app strings context is present & default).
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
}
@Test
@@ -243,6 +247,25 @@ class LocaleControllerTest {
assertThat(context.regionDefinition.regionId.ietfRegionTag).isEqualTo("MC")
}
+ @Test
+ fun testAppStringLocale_rootLocale_defaultLang_returnsRootLocale() {
+ forceDefaultLocale(Locale.ROOT)
+
+ val localeProvider = localeController.retrieveAppStringDisplayLocale(LANGUAGE_UNSPECIFIED)
+
+ // The locale will be forced to the root locale. The root locale also should never provide an
+ // IETF BCP-47 ID.
+ val locale = monitorFactory.waitForNextSuccessfulResult(localeProvider)
+ val context = locale.localeContext
+ val languageDefinition = context.languageDefinition
+ assertThat(languageDefinition.language).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(languageDefinition.fallbackMacroLanguage).isEqualTo(LANGUAGE_UNSPECIFIED)
+ assertThat(languageDefinition.appStringId.hasIetfBcp47Id()).isFalse()
+ assertThat(context.hasFallbackLanguageDefinition()).isFalse()
+ assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
+ assertThat(context.regionDefinition.regionId.ietfRegionTag).isEmpty()
+ }
+
@Test
fun testAppStringLocale_newSystemLocale_doesNotNotifyProvider() {
forceDefaultLocale(Locale.US)
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
index 195a53887db..b3b501ff94c 100644
--- a/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/translation/TranslationControllerTest.kt
@@ -19,6 +19,7 @@ import org.oppia.android.app.model.AppLanguageSelection.SelectionTypeCase.USE_SY
import org.oppia.android.app.model.AudioTranslationLanguageSelection
import org.oppia.android.app.model.HtmlTranslationList
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID
+import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.LanguageTypeCase.LANGUAGETYPE_NOT_SET
import org.oppia.android.app.model.OppiaLanguage
import org.oppia.android.app.model.OppiaLanguage.ARABIC
import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE
@@ -112,9 +113,7 @@ class TranslationControllerTest {
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(appStringId.languageTypeCase).isEqualTo(LANGUAGETYPE_NOT_SET)
assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
}
@@ -290,12 +289,28 @@ class TranslationControllerTest {
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(appStringId.languageTypeCase).isEqualTo(LANGUAGETYPE_NOT_SET)
assertThat(context.regionDefinition.region).isEqualTo(REGION_UNSPECIFIED)
}
+ @Test
+ fun testGetAppLanguageLocale_ptBrDefLocale_returnsLocaleWithIetfAndAndroidResourcesLangIds() {
+ forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
+
+ 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(BRAZILIAN_PORTUGUESE)
+ assertThat(appStringId.languageTypeCase).isEqualTo(IETF_BCP47_ID)
+ assertThat(appStringId.ietfBcp47Id.ietfLanguageTag).isEqualTo("pt-BR")
+ assertThat(appStringId.androidResourcesLanguageId.languageCode).isEqualTo("pt")
+ assertThat(appStringId.androidResourcesLanguageId.regionCode).isEqualTo("BR")
+ assertThat(context.regionDefinition.region).isEqualTo(BRAZIL)
+ }
+
@Test
fun testGetAppLanguageLocale_updateLanguageToEnglish_returnsEnglishLocale() {
forceDefaultLocale(Locale.ROOT)
@@ -1937,6 +1952,7 @@ class TranslationControllerTest {
private companion object {
private val BRAZIL_ENGLISH_LOCALE = Locale("en", "BR")
+ private val BRAZIL_PORTUGUESE_LOCALE = Locale("pt", "BR")
private val INDIA_HINDI_LOCALE = Locale("hi", "IN")
private val KENYA_KISWAHILI_LOCALE = Locale("sw", "KE")
diff --git a/model/src/main/proto/arguments.proto b/model/src/main/proto/arguments.proto
index dcee1263c71..d6ed3584dac 100644
--- a/model/src/main/proto/arguments.proto
+++ b/model/src/main/proto/arguments.proto
@@ -46,6 +46,18 @@ message InputInteractionViewTestActivityParams {
// Corresponds to the translation context which may affect the interaction's classifiers during
// tests.
WrittenTranslationContext written_translation_context = 2;
+}
+
+// TODO(#59): Isolate this to a test-only proto once possible.
+// Represents the parameters needed to open MathExpressionInteractionsViewTestActivity.
+message MathExpressionInteractionsViewTestActivityParams {
+ // Corresponds to the interaction used to initialize the interaction's view in the test
+ // environment.
+ Interaction interaction = 1;
+
+ // Corresponds to the translation context which may affect the interaction's classifiers during
+ // tests.
+ WrittenTranslationContext written_translation_context = 2;
// Indicates that a math interaction should be displayed for this activity, and indicates which
// one is being used in tests.
diff --git a/scripts/assets/accessibility_label_exemptions.textproto b/scripts/assets/accessibility_label_exemptions.textproto
index 4f2d9616133..2ff301e7c53 100644
--- a/scripts/assets/accessibility_label_exemptions.textproto
+++ b/scripts/assets/accessibility_label_exemptions.textproto
@@ -16,6 +16,7 @@ exempted_activity: "app/src/main/java/org/oppia/android/app/testing/DrawableBind
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ExplorationInjectionActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/FractionInputInteractionViewTestActivity"
+exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/HomeFragmentTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/HomeTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/HtmlParserTestActivity"
@@ -26,6 +27,7 @@ exempted_activity: "app/src/main/java/org/oppia/android/app/testing/RatioInputIn
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/LessonThumbnailImageViewTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ListItemLeadingMarginSpanTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/MarginBindingAdaptersTestActivity"
+exempted_activity: "app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/NavigationDrawerTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ProfileChooserFragmentTestActivity"
diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto
index 0ede1f683e0..67fbdc7d125 100644
--- a/scripts/assets/file_content_validation_checks.textproto
+++ b/scripts/assets/file_content_validation_checks.textproto
@@ -107,6 +107,11 @@ file_content_checks {
prohibited_content_regex: "translatable=\"false\""
failure_message: "Untranslatable strings should go in untranslated_strings.xml, instead."
}
+file_content_checks {
+ file_path_regex: "app/src/main/res/values/strings\\.xml"
+ prohibited_content_regex: "CDATA"
+ failure_message: "CDATA isn't handled by Translatewiki correctly. Use escaped HTML, instead."
+}
file_content_checks {
file_path_regex: ".+?\\.xml"
prohibited_content_regex: ""
@@ -599,3 +604,10 @@ file_content_checks {
exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt"
exempted_file_name: "testing/src/test/java/org/oppia/android/testing/espresso/TextInputActionTest.kt"
}
+file_content_checks {
+ file_path_regex: ".+?\\.kt"
+ prohibited_content_regex: "computeIfAbsent\\("
+ failure_message: "computeIfAbsent won't desugar and requires Java 8 support (SDK 24+). Suggest using an atomic Kotlin-specific solution, instead."
+ exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt"
+ exempted_file_name: "utility/src/main/java/org/oppia/android/util/caching/testing/FakeAssetRepository.kt"
+}
diff --git a/scripts/assets/maven_dependencies.textproto b/scripts/assets/maven_dependencies.textproto
index 841c9c94fa0..44a07d86bb4 100644
--- a/scripts/assets/maven_dependencies.textproto
+++ b/scripts/assets/maven_dependencies.textproto
@@ -1050,8 +1050,19 @@ maven_dependency {
}
}
maven_dependency {
- artifact_name: "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
- artifact_version: "1.4.1"
+ artifact_name: "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.3"
+ artifact_version: "1.4.3"
+ license {
+ license_name: "The Apache Software License, Version 2.0"
+ original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt"
+ scrapable_link {
+ url: "https://www.apache.org/licenses/LICENSE-2.0.txt"
+ }
+ }
+}
+maven_dependency {
+ artifact_name: "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
+ artifact_version: "1.4.3"
license {
license_name: "The Apache Software License, Version 2.0"
original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt"
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 46f9fc44462..54ae9c76830 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -13,6 +13,7 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/administratorcontro
exempted_file_path: "app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentPresenter.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/AdministratorControlsFragmentTestActivity.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/AdministratorControlsFragmentTestActivityPresenter.kt"
+exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/administratorcontrols/LoadAppVersionListener.kt"
exempted_file_path: "app/src/main/java/org/oppia/android/app/administratorcontrols/LoadLearnerAnalyticsListener.kt"
diff --git a/scripts/src/java/org/oppia/android/scripts/build/TransformAndroidManifest.kt b/scripts/src/java/org/oppia/android/scripts/build/TransformAndroidManifest.kt
index 4fde91e42eb..96095ee2925 100644
--- a/scripts/src/java/org/oppia/android/scripts/build/TransformAndroidManifest.kt
+++ b/scripts/src/java/org/oppia/android/scripts/build/TransformAndroidManifest.kt
@@ -1,6 +1,8 @@
package org.oppia.android.scripts.build
+import org.oppia.android.scripts.common.CommandExecutorImpl
import org.oppia.android.scripts.common.GitClient
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.w3c.dom.Document
import org.w3c.dom.NodeList
import java.io.File
@@ -62,21 +64,24 @@ fun main(args: Array) {
check(args.size >= 9) { USAGE_STRING }
val repoRoot = File(args[0]).also { if (!it.exists()) error("File doesn't exist: ${args[0]}") }
- TransformAndroidManifest(
- repoRoot = repoRoot,
- sourceManifestFile = File(args[1]).also {
- if (!it.exists()) {
- error("File doesn't exist: ${args[1]}")
- }
- },
- outputManifestFile = File(args[2]),
- buildFlavor = args[3],
- majorVersion = args[4].toIntOrNull() ?: error(USAGE_STRING),
- minorVersion = args[5].toIntOrNull() ?: error(USAGE_STRING),
- versionCode = args[6].toIntOrNull() ?: error(USAGE_STRING),
- relativelyQualifiedApplicationClass = args[7],
- baseDevelopBranchReference = args[8]
- ).generateAndOutputNewManifest()
+ ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
+ TransformAndroidManifest(
+ repoRoot = repoRoot,
+ sourceManifestFile = File(args[1]).also {
+ if (!it.exists()) {
+ error("File doesn't exist: ${args[1]}")
+ }
+ },
+ outputManifestFile = File(args[2]),
+ buildFlavor = args[3],
+ majorVersion = args[4].toIntOrNull() ?: error(USAGE_STRING),
+ minorVersion = args[5].toIntOrNull() ?: error(USAGE_STRING),
+ versionCode = args[6].toIntOrNull() ?: error(USAGE_STRING),
+ relativelyQualifiedApplicationClass = args[7],
+ baseDevelopBranchReference = args[8],
+ scriptBgDispatcher
+ ).generateAndOutputNewManifest()
+ }
}
private class TransformAndroidManifest(
@@ -88,11 +93,11 @@ private class TransformAndroidManifest(
private val minorVersion: Int,
private val versionCode: Int,
private val relativelyQualifiedApplicationClass: String,
- private val baseDevelopBranchReference: String
+ private val baseDevelopBranchReference: String,
+ private val scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher
) {
- private val gitClient by lazy {
- GitClient(repoRoot, baseDevelopBranchReference)
- }
+ private val commandExecutor by lazy { CommandExecutorImpl(scriptBgDispatcher) }
+ private val gitClient by lazy { GitClient(repoRoot, baseDevelopBranchReference, commandExecutor) }
private val documentBuilderFactory by lazy { DocumentBuilderFactory.newInstance() }
private val transformerFactory by lazy { TransformerFactory.newInstance() }
diff --git a/scripts/src/java/org/oppia/android/scripts/ci/ComputeAffectedTests.kt b/scripts/src/java/org/oppia/android/scripts/ci/ComputeAffectedTests.kt
index 5437874f191..ec82cb18e8b 100644
--- a/scripts/src/java/org/oppia/android/scripts/ci/ComputeAffectedTests.kt
+++ b/scripts/src/java/org/oppia/android/scripts/ci/ComputeAffectedTests.kt
@@ -5,6 +5,7 @@ import org.oppia.android.scripts.common.CommandExecutor
import org.oppia.android.scripts.common.CommandExecutorImpl
import org.oppia.android.scripts.common.GitClient
import org.oppia.android.scripts.common.ProtoStringEncoder.Companion.toCompressedBase64
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.proto.AffectedTestsBucket
import java.io.File
import java.util.Locale
@@ -59,9 +60,10 @@ fun main(args: Array) {
" '$computeAllTestsValue'"
)
}
- ComputeAffectedTests().compute(
- pathToRoot, pathToOutputFile, baseDevelopBranchReference, computeAllTestsSetting
- )
+ ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
+ ComputeAffectedTests(scriptBgDispatcher)
+ .compute(pathToRoot, pathToOutputFile, baseDevelopBranchReference, computeAllTestsSetting)
+ }
}
// Needed since the codebase isn't yet using Kotlin 1.5, so this function isn't available.
@@ -75,10 +77,11 @@ private fun String.toBooleanStrictOrNull(): Boolean? {
/** Utility used to compute affected test targets. */
class ComputeAffectedTests(
- private val maxTestCountPerLargeShard: Int = MAX_TEST_COUNT_PER_LARGE_SHARD,
- private val maxTestCountPerMediumShard: Int = MAX_TEST_COUNT_PER_MEDIUM_SHARD,
- private val maxTestCountPerSmallShard: Int = MAX_TEST_COUNT_PER_SMALL_SHARD,
- private val commandExecutor: CommandExecutor = CommandExecutorImpl()
+ private val scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher,
+ val maxTestCountPerLargeShard: Int = MAX_TEST_COUNT_PER_LARGE_SHARD,
+ val maxTestCountPerMediumShard: Int = MAX_TEST_COUNT_PER_MEDIUM_SHARD,
+ val maxTestCountPerSmallShard: Int = MAX_TEST_COUNT_PER_SMALL_SHARD,
+ val commandExecutor: CommandExecutor = CommandExecutorImpl(scriptBgDispatcher)
) {
private companion object {
private const val GENERIC_TEST_BUCKET_NAME = "generic"
@@ -108,7 +111,7 @@ class ComputeAffectedTests(
println("Running from directory root: $rootDirectory")
- val gitClient = GitClient(rootDirectory, baseDevelopBranchReference)
+ val gitClient = GitClient(rootDirectory, baseDevelopBranchReference, commandExecutor)
val bazelClient = BazelClient(rootDirectory, commandExecutor)
println("Current branch: ${gitClient.currentBranch}")
println("Most recent common commit: ${gitClient.branchMergeBase}")
diff --git a/scripts/src/java/org/oppia/android/scripts/common/BUILD.bazel b/scripts/src/java/org/oppia/android/scripts/common/BUILD.bazel
index 3a94254e9fc..6b26dd4f49f 100644
--- a/scripts/src/java/org/oppia/android/scripts/common/BUILD.bazel
+++ b/scripts/src/java/org/oppia/android/scripts/common/BUILD.bazel
@@ -36,6 +36,10 @@ kt_jvm_library(
"CommandResult.kt",
],
visibility = ["//scripts:oppia_script_library_visibility"],
+ deps = [
+ ":script_background_coroutine_dispatcher",
+ "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
+ ],
)
kt_jvm_library(
@@ -57,3 +61,12 @@ kt_jvm_library(
"//third_party:org_jetbrains_kotlin_kotlin-stdlib-jdk8_jar",
],
)
+
+kt_jvm_library(
+ name = "script_background_coroutine_dispatcher",
+ srcs = ["ScriptBackgroundCoroutineDispatcher.kt"],
+ visibility = ["//scripts:oppia_script_library_visibility"],
+ deps = [
+ "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
+ ],
+)
diff --git a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt
index b6071d86b76..7d5806a5869 100644
--- a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt
+++ b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt
@@ -10,7 +10,7 @@ import java.util.Locale
*/
class BazelClient(
private val rootDirectory: File,
- private val commandExecutor: CommandExecutor = CommandExecutorImpl()
+ private val commandExecutor: CommandExecutor
) {
/** Returns all Bazel test targets in the workspace. */
fun retrieveAllTestTargets(): List {
diff --git a/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt b/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt
index 44817344a8f..8431c534496 100644
--- a/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt
+++ b/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt
@@ -1,5 +1,8 @@
package org.oppia.android.scripts.common
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
import java.io.File
import java.util.concurrent.TimeUnit
@@ -11,6 +14,7 @@ const val WAIT_PROCESS_TIMEOUT_MS = 60_000L
/** Default implementation of [CommandExecutor]. */
class CommandExecutorImpl(
+ private val scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher,
private val processTimeout: Long = WAIT_PROCESS_TIMEOUT_MS,
private val processTimeoutUnit: TimeUnit = TimeUnit.MILLISECONDS
) : CommandExecutor {
@@ -29,7 +33,11 @@ class CommandExecutorImpl(
.directory(workingDir)
.redirectErrorStream(includeErrorOutput)
.start()
- val finished = process.waitFor(processTimeout, processTimeoutUnit)
+ val finished = runBlocking {
+ CoroutineScope(scriptBgDispatcher).async {
+ process.waitFor(processTimeout, processTimeoutUnit)
+ }.await()
+ }
check(finished) { "Process did not finish within the expected timeout" }
return CommandResult(
process.exitValue(),
diff --git a/scripts/src/java/org/oppia/android/scripts/common/GitClient.kt b/scripts/src/java/org/oppia/android/scripts/common/GitClient.kt
index 797d345bbf2..dfda3889b8f 100644
--- a/scripts/src/java/org/oppia/android/scripts/common/GitClient.kt
+++ b/scripts/src/java/org/oppia/android/scripts/common/GitClient.kt
@@ -9,10 +9,9 @@ import java.io.File
*/
class GitClient(
private val workingDirectory: File,
- private val baseDevelopBranchReference: String
+ private val baseDevelopBranchReference: String,
+ private val commandExecutor: CommandExecutor
) {
- private val commandExecutor by lazy { CommandExecutorImpl() }
-
/** The commit hash of the HEAD of the local Git repository. */
val currentCommit: String by lazy { retrieveCurrentCommit() }
diff --git a/scripts/src/java/org/oppia/android/scripts/common/ScriptBackgroundCoroutineDispatcher.kt b/scripts/src/java/org/oppia/android/scripts/common/ScriptBackgroundCoroutineDispatcher.kt
new file mode 100644
index 00000000000..52bb21c0a0f
--- /dev/null
+++ b/scripts/src/java/org/oppia/android/scripts/common/ScriptBackgroundCoroutineDispatcher.kt
@@ -0,0 +1,86 @@
+package org.oppia.android.scripts.common
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.asCoroutineDispatcher
+import java.io.Closeable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * A [CoroutineDispatcher] that's [Closeable] and particularly tailored to be easily used in scripts
+ * that need to perform parallel tasks for expensive IO. It's highly recommended to exclusively use
+ * this dispatcher over any others, and to ensure that [close] is called at the end of the script to
+ * avoid any potential threads hanging (causing the script to not actually close).
+ *
+ * Note that the dispatcher attempts to finish any ongoing tasks when [close] is called, but it will
+ * reject new tasks from being scheduled and it will force terminate if any pending tasks at the
+ * time of closing don't end within the configured [closeTimeout] provided.
+ *
+ * A simple example for using this dispatcher:
+ * ```kotlin
+ * ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
+ * val deferred = CoroutineScope(scriptBgDispatcher).async {
+ * // Expensive task...
+ * }
+ * // IMPORTANT: The operation must be observed before use{} ends, otherwise the dispatcher will
+ * // close and terminate any pending tasks.
+ * runBlocking { deferred.await() }
+ * }
+ * ```
+ *
+ * A more complex example for I/O operations:
+ * ```kotlin
+ * ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
+ * val deferred = CoroutineScope(scriptBgDispatcher).async {
+ * withContext(Dispatchers.IO) {
+ * // Perform I/O using Kotlin's highly parallelized I/O dispatcher, but wait for the result
+ * // using the background script dispatcher (since execution could continue if other I/O
+ * // operations need to be kicked off, or if other work can be done alongside the I/O).
+ * }
+ * }
+ * // IMPORTANT: The operation must be observed before use{} ends, otherwise the dispatcher will
+ * // close and terminate any pending tasks.
+ * runBlocking { deferred.await() }
+ * }
+ * ```
+ *
+ * @property closeTimeout the amount of time, in [closeTimeoutUnit] units, that should be waited
+ * when [close]ing this dispatcher before force-ending ongoing tasks
+ * @property closeTimeoutUnit the unit of time used for [closeTimeout]
+ */
+class ScriptBackgroundCoroutineDispatcher(
+ private val closeTimeout: Long = 5,
+ private val closeTimeoutUnit: TimeUnit = TimeUnit.SECONDS
+) : CoroutineDispatcher(), Closeable {
+ private val threadPool by lazy { Executors.newCachedThreadPool() }
+ private val coroutineDispatcher by lazy { threadPool.asCoroutineDispatcher() }
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ coroutineDispatcher.dispatch(context, block)
+ }
+
+ override fun close() {
+ threadPool.tryShutdownFully(timeout = closeTimeout, unit = closeTimeoutUnit)
+ coroutineDispatcher.close()
+ }
+
+ private companion object {
+ private fun ExecutorService.tryShutdownFully(timeout: Long, unit: TimeUnit) {
+ // Try to fully shutdown the executor service per https://stackoverflow.com/a/33690603 and
+ // https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorService.html.
+ shutdown()
+ try {
+ if (!awaitTermination(timeout, unit)) {
+ shutdownNow()
+ check(awaitTermination(timeout, unit)) { "ExecutorService didn't fully shutdown: $this." }
+ }
+ } catch (e: InterruptedException) {
+ shutdownNow()
+ Thread.currentThread().interrupt()
+ }
+ }
+ }
+}
diff --git a/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesListCheck.kt b/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesListCheck.kt
index 1632c3c9068..d8f178c7b21 100644
--- a/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesListCheck.kt
+++ b/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesListCheck.kt
@@ -3,6 +3,7 @@ package org.oppia.android.scripts.license
import com.google.protobuf.TextFormat
import org.oppia.android.scripts.common.CommandExecutor
import org.oppia.android.scripts.common.CommandExecutorImpl
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.proto.MavenDependency
/**
@@ -24,7 +25,9 @@ import org.oppia.android.scripts.proto.MavenDependency
* third_party/maven_install.json scripts/assets/maven_dependencies.pb
*/
fun main(args: Array) {
- MavenDependenciesListCheck(LicenseFetcherImpl()).main(args)
+ ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
+ MavenDependenciesListCheck(LicenseFetcherImpl(), scriptBgDispatcher).main(args)
+ }
}
/**
@@ -33,7 +36,8 @@ fun main(args: Array) {
*/
class MavenDependenciesListCheck(
private val licenseFetcher: LicenseFetcher,
- private val commandExecutor: CommandExecutor = CommandExecutorImpl()
+ scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher,
+ private val commandExecutor: CommandExecutor = CommandExecutorImpl(scriptBgDispatcher)
) {
/**
diff --git a/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesRetriever.kt b/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesRetriever.kt
index 3b67d51a751..e37c188bac5 100644
--- a/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesRetriever.kt
+++ b/scripts/src/java/org/oppia/android/scripts/license/MavenDependenciesRetriever.kt
@@ -5,7 +5,6 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import org.oppia.android.scripts.common.BazelClient
import org.oppia.android.scripts.common.CommandExecutor
-import org.oppia.android.scripts.common.CommandExecutorImpl
import org.oppia.android.scripts.maven.model.MavenListDependency
import org.oppia.android.scripts.maven.model.MavenListDependencyTree
import org.oppia.android.scripts.proto.License
@@ -24,7 +23,7 @@ private const val MAVEN_PREFIX = "@maven//:"
class MavenDependenciesRetriever(
private val rootPath: String,
private val licenseFetcher: LicenseFetcher,
- private val commandExecutor: CommandExecutor = CommandExecutorImpl()
+ private val commandExecutor: CommandExecutor
) {
private val bazelClient by lazy {
diff --git a/scripts/src/java/org/oppia/android/scripts/maven/GenerateMavenDependenciesList.kt b/scripts/src/java/org/oppia/android/scripts/maven/GenerateMavenDependenciesList.kt
index 8829a55a475..36b28f3228c 100644
--- a/scripts/src/java/org/oppia/android/scripts/maven/GenerateMavenDependenciesList.kt
+++ b/scripts/src/java/org/oppia/android/scripts/maven/GenerateMavenDependenciesList.kt
@@ -2,6 +2,7 @@ package org.oppia.android.scripts.maven
import org.oppia.android.scripts.common.CommandExecutor
import org.oppia.android.scripts.common.CommandExecutorImpl
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.license.LicenseFetcher
import org.oppia.android.scripts.license.LicenseFetcherImpl
import org.oppia.android.scripts.license.MavenDependenciesRetriever
@@ -27,13 +28,16 @@ import org.oppia.android.scripts.proto.MavenDependencyList
* scripts/assets/maven_dependencies.pb
*/
fun main(args: Array) {
- GenerateMavenDependenciesList(LicenseFetcherImpl()).main(args)
+ ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
+ GenerateMavenDependenciesList(LicenseFetcherImpl(), scriptBgDispatcher).main(args)
+ }
}
/** Wrapper class to pass dependencies to be utilized by the the main method. */
class GenerateMavenDependenciesList(
private val licenseFetcher: LicenseFetcher,
- private val commandExecutor: CommandExecutor = CommandExecutorImpl()
+ scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher,
+ private val commandExecutor: CommandExecutor = CommandExecutorImpl(scriptBgDispatcher)
) {
/**
* Compiles a list of third-party maven dependencies along with their license links on
@@ -48,45 +52,46 @@ class GenerateMavenDependenciesList(
val pathToMavenDependenciesTextProto = "$pathToRoot/${args[2]}"
val pathToMavenDependenciesPb = args[3]
- val MavenDependenciesRetriever = MavenDependenciesRetriever(pathToRoot, licenseFetcher)
+ val mavenDependenciesRetriever =
+ MavenDependenciesRetriever(pathToRoot, licenseFetcher, commandExecutor)
val bazelQueryDepsList =
- MavenDependenciesRetriever.retrieveThirdPartyMavenDependenciesList()
- val mavenInstallDepsList = MavenDependenciesRetriever.getDependencyListFromMavenInstall(
+ mavenDependenciesRetriever.retrieveThirdPartyMavenDependenciesList()
+ val mavenInstallDepsList = mavenDependenciesRetriever.getDependencyListFromMavenInstall(
pathToMavenInstallJson,
bazelQueryDepsList
)
val dependenciesListFromPom =
- MavenDependenciesRetriever
+ mavenDependenciesRetriever
.retrieveDependencyListFromPom(mavenInstallDepsList)
.mavenDependencyList
val dependenciesListFromTextProto =
- MavenDependenciesRetriever.retrieveMavenDependencyList(pathToMavenDependenciesPb)
+ mavenDependenciesRetriever.retrieveMavenDependencyList(pathToMavenDependenciesPb)
- val updatedDependenciesList = MavenDependenciesRetriever.addChangesFromTextProto(
+ val updatedDependenciesList = mavenDependenciesRetriever.addChangesFromTextProto(
dependenciesListFromPom,
dependenciesListFromTextProto
)
val manuallyUpdatedLicenses =
- MavenDependenciesRetriever.retrieveManuallyUpdatedLicensesSet(updatedDependenciesList)
+ mavenDependenciesRetriever.retrieveManuallyUpdatedLicensesSet(updatedDependenciesList)
- val finalDependenciesList = MavenDependenciesRetriever.updateMavenDependenciesList(
+ val finalDependenciesList = mavenDependenciesRetriever.updateMavenDependenciesList(
updatedDependenciesList,
manuallyUpdatedLicenses
)
- MavenDependenciesRetriever.writeTextProto(
+ mavenDependenciesRetriever.writeTextProto(
pathToMavenDependenciesTextProto,
MavenDependencyList.newBuilder().addAllMavenDependency(finalDependenciesList).build()
)
val licensesToBeFixed =
- MavenDependenciesRetriever.getAllBrokenLicenses(finalDependenciesList)
+ mavenDependenciesRetriever.getAllBrokenLicenses(finalDependenciesList)
if (licensesToBeFixed.isNotEmpty()) {
- val licenseToDependencyMap = MavenDependenciesRetriever
+ val licenseToDependencyMap = mavenDependenciesRetriever
.findFirstDependenciesWithBrokenLicenses(
finalDependenciesList,
licensesToBeFixed
@@ -95,34 +100,34 @@ class GenerateMavenDependenciesList(
"""
Some licenses do not have their 'original_link' verified. To verify a license link, click
on the original link of the license and check if the link points to any valid license or
- not. If the link does not point to a valid license (e.g - https://fabric.io/terms), set
+ not. If the link does not point to a valid license (e.g. - https://fabric.io/terms), set
the 'is_original_link_invalid' field of the license to 'true'.
-
- e.g -
+
+ e.g. -
license {
license_name: "Terms of Service for Firebase Services"
original_link: "https://fabric.io/terms"
is_original_link_invalid: true
}
-
- If the link does point to a valid license then choose the most appropriate category for
+
+ If the link does point to a valid license then choose the most appropriate category for
the link:
-
+
1. scrapable_link: If the license text is plain text and the URL mentioned can be scraped
- directly from the original_link of the license. e.g -
+ directly from the original_link of the license. e.g. -
https://www.apache.org/licenses/LICENSE-2.0.txt
-
- 2. extracted_copy_link: If the license text is plain text but it can not be scraped
- directly from the original_link of the license. e.g -
+
+ 2. extracted_copy_link: If the license text is plain text but it can not be scraped
+ directly from the original_link of the license. e.g. -
https://www.opensource.org/licenses/bsd-license
-
+
3. direct_link_only: If the license text is not plain text, it's best to display only the
- link of the license. e.g - https://developer.android.com/studio/terms.html
-
+ link of the license. e.g. - https://developer.android.com/studio/terms.html
+
After identifying the category of the license, modify the license to include one of the
- above mentioned 'url'.
-
- e.g -
+ above mentioned 'url'.
+
+ e.g. -
license {
license_name: "The Apache Software License, Version 2.0"
original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt"
@@ -130,9 +135,9 @@ class GenerateMavenDependenciesList(
url: "https://www.apache.org/licenses/LICENSE-2.0.txt"
}
}
-
- Please verify the license link(s) for the following license(s) manually in
- maven_dependencies.textproto. Note that only first dependency that contains the license
+
+ Please verify the license link(s) for the following license(s) manually in
+ maven_dependencies.textproto. Note that only first dependency that contains the license
needs to be updated and also re-run the script to update the license details at all places.
""".trimIndent()
)
@@ -150,21 +155,21 @@ class GenerateMavenDependenciesList(
}
val dependenciesWithoutAnyLinks =
- MavenDependenciesRetriever.getDependenciesThatNeedIntervention(finalDependenciesList)
+ mavenDependenciesRetriever.getDependenciesThatNeedIntervention(finalDependenciesList)
if (dependenciesWithoutAnyLinks.isNotEmpty()) {
println(
"""
- Please remove all the invalid links (if any) from maven_dependencies.textproto for the
+ Please remove all the invalid links (if any) from maven_dependencies.textproto for the
below mentioned dependencies and provide the valid license links manually.
- e.g -
-
+ e.g. -
+
maven_dependency {
artifact_name: "com.google.guava:failureaccess:1.0.1"
artifact_version: "1.0.1"
}
-
+
***** changes to *****
-
+
maven_dependency {
artifact_name: "com.google.guava:failureaccess:1.0.1"
artifact_version: "1.0.1"
@@ -175,7 +180,7 @@ class GenerateMavenDependenciesList(
}
}
}
-
+
Dependencies with invalid or no license links:
""".trimIndent() + "\n"
)
diff --git a/scripts/src/javatests/org/oppia/android/scripts/build/TransformAndroidManifestTest.kt b/scripts/src/javatests/org/oppia/android/scripts/build/TransformAndroidManifestTest.kt
index 5d197b04615..d614ef5a1db 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/build/TransformAndroidManifestTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/build/TransformAndroidManifestTest.kt
@@ -1,11 +1,13 @@
package org.oppia.android.scripts.build
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.oppia.android.scripts.common.CommandExecutorImpl
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.testing.TestGitRepository
import org.oppia.android.testing.assertThrows
import java.io.File
@@ -54,16 +56,20 @@ class TransformAndroidManifestTest {
private val VERSION_CODE = "23"
private val APPLICATION_RELATIVE_QUALIFIED_CLASS = ".example.CustomApplication"
- @Rule
- @JvmField
- var tempFolder = TemporaryFolder()
+ @field:[Rule JvmField] val tempFolder = TemporaryFolder()
- private val commandExecutor by lazy { CommandExecutorImpl() }
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
+ private val commandExecutor by lazy { CommandExecutorImpl(scriptBgDispatcher) }
private lateinit var testGitRepository: TestGitRepository
@Before
fun setUp() {
- testGitRepository = TestGitRepository(tempFolder, CommandExecutorImpl())
+ testGitRepository = TestGitRepository(tempFolder, commandExecutor)
+ }
+
+ @After
+ fun tearDown() {
+ scriptBgDispatcher.close()
}
@Test
diff --git a/scripts/src/javatests/org/oppia/android/scripts/ci/ComputeAffectedTestsTest.kt b/scripts/src/javatests/org/oppia/android/scripts/ci/ComputeAffectedTestsTest.kt
index 2bf59212f66..4d8542e1e5b 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/ci/ComputeAffectedTestsTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/ci/ComputeAffectedTestsTest.kt
@@ -6,8 +6,10 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
+import org.oppia.android.scripts.common.CommandExecutor
import org.oppia.android.scripts.common.CommandExecutorImpl
import org.oppia.android.scripts.common.ProtoStringEncoder.Companion.mergeFromCompressedBase64
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.proto.AffectedTestsBucket
import org.oppia.android.scripts.testing.TestBazelWorkspace
import org.oppia.android.scripts.testing.TestGitRepository
@@ -31,8 +33,9 @@ import java.util.concurrent.TimeUnit
class ComputeAffectedTestsTest {
@field:[Rule JvmField] val tempFolder = TemporaryFolder()
- private val commandExecutor by lazy { initiazeCommandExecutorWithLongProcessWaitTime() }
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
+ private lateinit var commandExecutor: CommandExecutor
private lateinit var testBazelWorkspace: TestBazelWorkspace
private lateinit var testGitRepository: TestGitRepository
private lateinit var pendingOutputStream: ByteArrayOutputStream
@@ -40,8 +43,9 @@ class ComputeAffectedTestsTest {
@Before
fun setUp() {
+ commandExecutor = initializeCommandExecutorWithLongProcessWaitTime()
testBazelWorkspace = TestBazelWorkspace(tempFolder)
- testGitRepository = TestGitRepository(tempFolder, CommandExecutorImpl())
+ testGitRepository = TestGitRepository(tempFolder, commandExecutor)
// Redirect script output for testing purposes.
pendingOutputStream = ByteArrayOutputStream()
@@ -58,6 +62,8 @@ class ComputeAffectedTestsTest {
// and to help manually verify the expect git state at the end of each test.
println("git status (at end of test):")
println(testGitRepository.status(checkForGitRepository = false))
+
+ scriptBgDispatcher.close()
}
@Test
@@ -722,6 +728,7 @@ class ComputeAffectedTestsTest {
// Note that main() can't be used since the shard counts need to be overwritten. Dagger would
// be a nicer means to do this, but it's not set up currently for scripts.
ComputeAffectedTests(
+ scriptBgDispatcher,
maxTestCountPerLargeShard = maxTestCountPerLargeShard,
maxTestCountPerMediumShard = maxTestCountPerMediumShard,
maxTestCountPerSmallShard = maxTestCountPerSmallShard,
@@ -864,7 +871,9 @@ class ComputeAffectedTestsTest {
testGitRepository.commit(message = "Modified library $name")
}
- private fun initiazeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl {
- return CommandExecutorImpl(processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES)
+ private fun initializeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl {
+ return CommandExecutorImpl(
+ scriptBgDispatcher, processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES
+ )
}
}
diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel
index bceb5cfe864..bb2009d98bd 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel
+++ b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel
@@ -61,3 +61,14 @@ kt_jvm_test(
"//third_party:org_jetbrains_kotlin_kotlin-test-junit",
],
)
+
+kt_jvm_test(
+ name = "ScriptBackgroundCoroutineDispatcherTest",
+ srcs = ["ScriptBackgroundCoroutineDispatcherTest.kt"],
+ deps = [
+ "//scripts/src/java/org/oppia/android/scripts/common:script_background_coroutine_dispatcher",
+ "//testing:assertion_helpers",
+ "//third_party:org_jetbrains_kotlin_kotlin-test-junit",
+ "//third_party:org_mockito_mockito-core",
+ ],
+)
diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt b/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt
index 2109155025f..57b2f26964a 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt
@@ -1,6 +1,7 @@
package org.oppia.android.scripts.common
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -28,10 +29,17 @@ class BazelClientTest {
@field:[Rule JvmField] val tempFolder = TemporaryFolder()
@field:[Rule JvmField] val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
+ private val commandExecutor by lazy { CommandExecutorImpl(scriptBgDispatcher) }
+ private val longCommandExecutor by lazy { initializeCommandExecutorWithLongProcessWaitTime() }
+ private lateinit var testBazelWorkspace: TestBazelWorkspace
+
@Mock lateinit var mockCommandExecutor: CommandExecutor
- private val commandExecutor by lazy { initiazeCommandExecutorWithLongProcessWaitTime() }
- private lateinit var testBazelWorkspace: TestBazelWorkspace
+ @After
+ fun tearDown() {
+ scriptBgDispatcher.close()
+ }
@Before
fun setUp() {
@@ -335,7 +343,7 @@ class BazelClientTest {
artifactName = "com.android.support:support-annotations:28.0.0",
buildFile = thirdPartyBuild
)
- val bazelClient = BazelClient(tempFolder.root, commandExecutor)
+ val bazelClient = BazelClient(tempFolder.root, longCommandExecutor)
val thirdPartyDependenciesList =
bazelClient.retrieveThirdPartyMavenDepsListForBinary("//:test_oppia")
@@ -344,7 +352,7 @@ class BazelClientTest {
}
@Test
- fun testRetrieveMavenDepsList_binaryDependsOnArtifactNotViaThirdParty_doesNotreturnArtifact() {
+ fun testRetrieveMavenDepsList_binaryDependsOnArtifactNotViaThirdParty_doesNotReturnArtifact() {
testBazelWorkspace.initEmptyWorkspace()
testBazelWorkspace.setUpWorkspaceForRulesJvmExternal(
listOf("com.android.support:support-annotations:28.0.0")
@@ -365,7 +373,7 @@ class BazelClientTest {
artifactName = "com.android.support:support-annotations:28.0.0",
buildFile = testBazelWorkspace.rootBuildFile
)
- val bazelClient = BazelClient(tempFolder.root, commandExecutor)
+ val bazelClient = BazelClient(tempFolder.root, longCommandExecutor)
val thirdPartyDependenciesList =
bazelClient.retrieveThirdPartyMavenDepsListForBinary("//:test_oppia")
@@ -457,8 +465,10 @@ class BazelClientTest {
return secondNewFile
}
- private fun initiazeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl {
- return CommandExecutorImpl(processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES)
+ private fun initializeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl {
+ return CommandExecutorImpl(
+ scriptBgDispatcher, processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES
+ )
}
private fun updateBuildFileToUseCustomJvmTestRule(bazelFile: File, buildFile: File) {
diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/CommandExecutorImplTest.kt b/scripts/src/javatests/org/oppia/android/scripts/common/CommandExecutorImplTest.kt
index 78dc945dff6..140c27bfe1d 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/common/CommandExecutorImplTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/common/CommandExecutorImplTest.kt
@@ -1,6 +1,7 @@
package org.oppia.android.scripts.common
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -19,13 +20,18 @@ import java.util.concurrent.TimeUnit
// Function name: test names are conventionally named with underscores.
@Suppress("FunctionName")
class CommandExecutorImplTest {
- @Rule
- @JvmField
- var tempFolder = TemporaryFolder()
+ @field:[Rule JvmField] val tempFolder = TemporaryFolder()
+
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
+
+ @After
+ fun tearDown() {
+ scriptBgDispatcher.close()
+ }
@Test
fun testExecute_echo_oneArgument_succeedsWithOutput() {
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val result = commandExecutor.executeCommand(tempFolder.root, "echo", "value")
@@ -35,7 +41,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_echo_invalidDirectory_throwsException() {
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val exception = assertThrows() {
commandExecutor.executeCommand(File("invaliddirectory"), "echo", "value")
@@ -47,7 +53,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_echo_largeOutput_insufficientTimeout_throwsException() {
val commandExecutor = CommandExecutorImpl(
- processTimeout = 0L, processTimeoutUnit = TimeUnit.MILLISECONDS
+ scriptBgDispatcher, processTimeout = 0L, processTimeoutUnit = TimeUnit.MILLISECONDS
)
// Produce a large output so that echo takes a bit longer to reduce the likelihood of this test
@@ -63,7 +69,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_nonexistentCommand_throwsException() {
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val exception = assertThrows() {
commandExecutor.executeCommand(tempFolder.root, "commanddoesnotexist")
@@ -74,7 +80,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_echo_multipleArguments_succeedsWithOutput() {
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val result = commandExecutor.executeCommand(tempFolder.root, "echo", "first", "second", "third")
@@ -84,7 +90,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_echo_multipleArguments_resultHasCorrectCommand() {
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val result = commandExecutor.executeCommand(tempFolder.root, "echo", "first", "second", "third")
@@ -93,7 +99,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_defaultErrorOutput_rmdir_failed_failsWithCombinedOutput() {
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val result = commandExecutor.executeCommand(tempFolder.root, "rmdir", "filethatdoesnotexist")
@@ -105,7 +111,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_splitErrorOutput_rmdir_failed_failsWithErrorOutput() {
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val result =
commandExecutor.executeCommand(
@@ -121,7 +127,7 @@ class CommandExecutorImplTest {
@Test
fun testExecute_removeDirectoryInLocalDirectory_succeeds() {
val newFolder = tempFolder.newFolder("newfolder")
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val result = commandExecutor.executeCommand(tempFolder.root, "rmdir", "./newfolder")
@@ -135,7 +141,7 @@ class CommandExecutorImplTest {
fun testExecute_removeUnknownDirectoryInOtherDirectory_fails() {
val newFolder = tempFolder.newFolder("newfolder")
val alternateRoot = tempFolder.newFolder("alternateroot")
- val commandExecutor = CommandExecutorImpl()
+ val commandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val result = commandExecutor.executeCommand(alternateRoot, "rmdir", "./newfolder")
diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/GitClientTest.kt b/scripts/src/javatests/org/oppia/android/scripts/common/GitClientTest.kt
index c3126339eca..2741900296b 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/common/GitClientTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/common/GitClientTest.kt
@@ -20,12 +20,11 @@ import java.lang.IllegalStateException
// Function name: test names are conventionally named with underscores.
@Suppress("FunctionName")
class GitClientTest {
- @Rule
- @JvmField
- var tempFolder = TemporaryFolder()
+ @field:[Rule JvmField] val tempFolder = TemporaryFolder()
private lateinit var testGitRepository: TestGitRepository
- private val commandExecutor by lazy { CommandExecutorImpl() }
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
+ private val commandExecutor by lazy { CommandExecutorImpl(scriptBgDispatcher) }
@Before
fun setUp() {
@@ -38,11 +37,12 @@ class GitClientTest {
// and to help manually verify the expected git state at the end of each test.
println("git status (at end of test):")
println(testGitRepository.status(checkForGitRepository = false))
+ scriptBgDispatcher.close()
}
@Test
fun testCurrentCommit_forNonRepository_throwsException() {
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val exception = assertThrows() { gitClient.currentCommit }
@@ -55,7 +55,7 @@ class GitClientTest {
initializeRepoWithDevelopBranch()
val developHash = getMostRecentCommitOnCurrentBranch()
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val currentCommit = gitClient.currentCommit
assertThat(currentCommit).isEqualTo(developHash)
@@ -69,7 +69,7 @@ class GitClientTest {
testGitRepository.commit(message = "Test empty commit", allowEmpty = true)
val featureBranchHash = getMostRecentCommitOnCurrentBranch()
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val currentCommit = gitClient.currentCommit
assertThat(currentCommit).isNotEqualTo(developHash)
@@ -78,7 +78,7 @@ class GitClientTest {
@Test
fun testCurrentBranch_forNonRepository_throwsException() {
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val exception = assertThrows() { gitClient.currentBranch }
@@ -90,7 +90,7 @@ class GitClientTest {
fun testCurrentBranch_forValidRepository_returnsCorrectBranch() {
initializeRepoWithDevelopBranch()
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val currentBranch = gitClient.currentBranch
assertThat(currentBranch).isEqualTo("develop")
@@ -101,7 +101,7 @@ class GitClientTest {
initializeRepoWithDevelopBranch()
testGitRepository.checkoutNewBranch("introduce-feature")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val currentBranch = gitClient.currentBranch
assertThat(currentBranch).isEqualTo("introduce-feature")
@@ -112,7 +112,7 @@ class GitClientTest {
initializeRepoWithDevelopBranch()
val developHash = getMostRecentCommitOnCurrentBranch()
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val mergeBase = gitClient.branchMergeBase
assertThat(mergeBase).isEqualTo(developHash)
@@ -124,7 +124,7 @@ class GitClientTest {
val developHash = getMostRecentCommitOnCurrentBranch()
testGitRepository.checkoutNewBranch("introduce-feature")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val mergeBase = gitClient.branchMergeBase
assertThat(mergeBase).isEqualTo(developHash)
@@ -137,7 +137,7 @@ class GitClientTest {
testGitRepository.checkoutNewBranch("introduce-feature")
commitNewFile("example_file")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val mergeBase = gitClient.branchMergeBase
// The merge base is the latest common hash between this & the develop branch.
@@ -149,7 +149,7 @@ class GitClientTest {
initializeRepoWithDevelopBranch()
testGitRepository.checkoutNewBranch("introduce-feature")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
assertThat(changedFiles).isEmpty()
@@ -161,7 +161,7 @@ class GitClientTest {
testGitRepository.checkoutNewBranch("introduce-feature")
createNewFile("example_file")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
assertThat(changedFiles).containsExactly("example_file")
@@ -173,7 +173,7 @@ class GitClientTest {
testGitRepository.checkoutNewBranch("introduce-feature")
stageNewFile("example_file")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
assertThat(changedFiles).containsExactly("example_file")
@@ -185,7 +185,7 @@ class GitClientTest {
testGitRepository.checkoutNewBranch("introduce-feature")
commitNewFile("example_file")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
assertThat(changedFiles).containsExactly("example_file")
@@ -197,7 +197,7 @@ class GitClientTest {
commitNewFile("develop_file")
testGitRepository.checkoutNewBranch("introduce-feature")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
// Committed files to the develop branch are not included since they aren't part of the feature
@@ -212,7 +212,7 @@ class GitClientTest {
commitNewFile("example_file")
modifyFile("example_file")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
assertThat(changedFiles).containsExactly("example_file")
@@ -225,7 +225,7 @@ class GitClientTest {
testGitRepository.checkoutNewBranch("introduce-feature")
deleteFile("develop_file")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
assertThat(changedFiles).containsExactly("develop_file")
@@ -244,7 +244,7 @@ class GitClientTest {
modifyFile("develop_branch_file_changed_not_staged")
deleteFile("develop_branch_file_removed")
- val gitClient = GitClient(tempFolder.root, "develop")
+ val gitClient = GitClient(tempFolder.root, "develop", commandExecutor)
val changedFiles = gitClient.changedFiles
assertThat(changedFiles).containsExactly(
diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/ScriptBackgroundCoroutineDispatcherTest.kt b/scripts/src/javatests/org/oppia/android/scripts/common/ScriptBackgroundCoroutineDispatcherTest.kt
new file mode 100644
index 00000000000..c1d4f98314b
--- /dev/null
+++ b/scripts/src/javatests/org/oppia/android/scripts/common/ScriptBackgroundCoroutineDispatcherTest.kt
@@ -0,0 +1,81 @@
+package org.oppia.android.scripts.common
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.oppia.android.testing.assertThrows
+import java.util.concurrent.TimeUnit
+
+/** Tests for [ScriptBackgroundCoroutineDispatcher]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+class ScriptBackgroundCoroutineDispatcherTest {
+ @field:[Rule JvmField] val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock lateinit var mockRunnable: Runnable
+
+ @Test
+ fun testDispatchTask_taskIsRun() {
+ val dispatcher = ScriptBackgroundCoroutineDispatcher()
+
+ runBlocking { withContext(dispatcher) { mockRunnable.run() } }
+
+ verify(mockRunnable).run()
+ }
+
+ @Test
+ fun testClose_noExceptionThrown() {
+ val dispatcher = ScriptBackgroundCoroutineDispatcher()
+
+ dispatcher.close()
+
+ // The verification is that no exception is thrown (otherwise the test should fail).
+ }
+
+ @Test
+ fun testDispatch_afterClosing_throwsException() {
+ val dispatcher = ScriptBackgroundCoroutineDispatcher()
+ dispatcher.close()
+
+ // The task should fail to schedule since the dispatcher has been closed.
+ assertThrows() {
+ runBlocking { withContext(dispatcher) { mockRunnable.run() } }
+ }
+ }
+
+ @Test
+ fun testClose_pendingTaskLongerThanCloseTimeout_taskIsNotRun() {
+ val dispatcher =
+ ScriptBackgroundCoroutineDispatcher(
+ closeTimeout = 50L, closeTimeoutUnit = TimeUnit.MILLISECONDS
+ )
+ val taskStartedChannel = Channel()
+ // Schedule a task but make sure that the attempt to close the dispatcher happens exactly
+ // between the task starting and ending (to verify close timeout flows).
+ @Suppress("DeferredResultUnused")
+ CoroutineScope(dispatcher).async {
+ taskStartedChannel.send(true)
+ delay(1_000L)
+ mockRunnable.run()
+ }
+ runBlocking { taskStartedChannel.receive() }
+
+ dispatcher.close()
+ // This slows down the test, but provides assurance that the task was definitely cancelled.
+ runBlocking { delay(2_000L) }
+
+ // The task should not have run since it was cancelled, but no exception will be thrown.
+ verifyNoMoreInteractions(mockRunnable)
+ }
+}
diff --git a/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesListCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesListCheckTest.kt
index 5fdc807e3db..6ecefd7ddce 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesListCheckTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesListCheckTest.kt
@@ -10,6 +10,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.oppia.android.scripts.common.CommandExecutorImpl
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.proto.DirectLinkOnly
import org.oppia.android.scripts.proto.ExtractedCopyLink
import org.oppia.android.scripts.proto.License
@@ -79,16 +80,16 @@ class MavenDependenciesListCheckTest {
private val UNAVAILABLE_OR_INVALID_LICENSE_LINKS_FAILURE =
"License links are invalid or not available for some dependencies"
+ @field:[Rule JvmField] val tempFolder = TemporaryFolder()
+
private val outContent: ByteArrayOutputStream = ByteArrayOutputStream()
private val originalOut: PrintStream = System.out
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
private val mockLicenseFetcher by lazy { initializeLicenseFetcher() }
private val commandExecutor by lazy { initializeCommandExecutorWithLongProcessWaitTime() }
- private lateinit var testBazelWorkspace: TestBazelWorkspace
- @Rule
- @JvmField
- var tempFolder = TemporaryFolder()
+ private lateinit var testBazelWorkspace: TestBazelWorkspace
@Before
fun setUp() {
@@ -101,6 +102,7 @@ class MavenDependenciesListCheckTest {
@After
fun restoreStreams() {
System.setOut(originalOut)
+ scriptBgDispatcher.close()
}
@Test
@@ -113,6 +115,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -135,14 +138,14 @@ class MavenDependenciesListCheckTest {
license_name: "The Apache License, Version 2.0"
original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt"
}
-
+
artifact_name: "com.google.firebase:firebase-analytics:17.5.0"
artifact_version: "17.5.0"
license {
license_name: "Android Software Development Kit License"
original_link: "https://developer.android.com/studio/terms.html"
}
-
+
Refer to https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies for more details.
""".trimIndent() + "\n"
)
@@ -189,6 +192,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -211,7 +215,7 @@ class MavenDependenciesListCheckTest {
license_name: "Android Software Development Kit License"
original_link: "https://developer.android.com/studio/terms.html"
}
-
+
Refer to https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies for more details.
""".trimIndent() + "\n"
)
@@ -253,6 +257,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -275,14 +280,14 @@ class MavenDependenciesListCheckTest {
license_name: "The Apache License, Version 2.0"
original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt"
}
-
+
artifact_name: "$FIREBASE_ANALYTICS_DEP"
artifact_version: "$FIREBASE_ANALYTICS_VERSION"
license {
license_name: "Android Software Development Kit License"
original_link: "https://developer.android.com/studio/terms.html"
}
-
+
Refer to https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies for more details.
""".trimIndent() + "\n"
)
@@ -329,6 +334,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -408,6 +414,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -430,7 +437,7 @@ class MavenDependenciesListCheckTest {
license_name: "The Apache License, Version 2.0"
original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt"
}
-
+
artifact_name: "$FIREBASE_ANALYTICS_DEP"
artifact_version: "$FIREBASE_ANALYTICS_VERSION"
license {
@@ -484,6 +491,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -508,14 +516,14 @@ class MavenDependenciesListCheckTest {
}
Missing dependencies that need to be added:
-
+
artifact_name: "$FIREBASE_ANALYTICS_DEP"
artifact_version: "$FIREBASE_ANALYTICS_VERSION"
license {
license_name: "Android Software Development Kit License"
original_link: "https://developer.android.com/studio/terms.html"
}
-
+
Refer to https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies for more details.
""".trimIndent() + "\n"
)
@@ -562,6 +570,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -586,14 +595,14 @@ class MavenDependenciesListCheckTest {
}
Missing dependencies that need to be added:
-
+
artifact_name: "$FIREBASE_ANALYTICS_UPGRADED_DEP"
artifact_version: "$FIREBASE_ANALYTICS_UPGRADED_VERSION"
license {
license_name: "Android Software Development Kit License"
original_link: "https://developer.android.com/studio/terms.html"
}
-
+
Refer to https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies for more details.
""".trimIndent() + "\n"
)
@@ -640,6 +649,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -651,7 +661,7 @@ class MavenDependenciesListCheckTest {
}
assertThat(exception).hasMessageThat().contains(MISSING_AND_REDUNDANT_DEPENDENCIES_FAILURE)
assertThat(outContent.toString()).isEqualTo(
- """
+ """
Errors were encountered. Please run script GenerateMavenDependenciesList.kt to fix.
Redundant dependencies that need to be removed:
@@ -664,14 +674,14 @@ class MavenDependenciesListCheckTest {
}
Missing dependencies that need to be added:
-
+
artifact_name: "$FIREBASE_ANALYTICS_DEP"
artifact_version: "$FIREBASE_ANALYTICS_VERSION"
license {
license_name: "Android Software Development Kit License"
original_link: "https://developer.android.com/studio/terms.html"
}
-
+
Refer to https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies for more details.
""".trimIndent() + "\n"
)
@@ -720,6 +730,7 @@ class MavenDependenciesListCheckTest {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -773,6 +784,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -822,6 +834,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -877,6 +890,7 @@ class MavenDependenciesListCheckTest {
val exception = assertThrows() {
MavenDependenciesListCheck(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -1018,13 +1032,15 @@ class MavenDependenciesListCheckTest {
}
]
}
- }
+ }
""".trimIndent()
)
}
private fun initializeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl {
- return CommandExecutorImpl(processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES)
+ return CommandExecutorImpl(
+ scriptBgDispatcher, processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES
+ )
}
/** Returns a mock for the [LicenseFetcher]. */
diff --git a/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesRetrieverTest.kt b/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesRetrieverTest.kt
index 5dd8baec8b3..311bfe5f446 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesRetrieverTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/license/MavenDependenciesRetrieverTest.kt
@@ -11,6 +11,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.oppia.android.scripts.common.CommandExecutorImpl
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.maven.model.MavenListDependency
import org.oppia.android.scripts.proto.DirectLinkOnly
import org.oppia.android.scripts.proto.ExtractedCopyLink
@@ -81,12 +82,11 @@ class MavenDependenciesRetrieverTest {
private val mavenDependenciesRetriever by lazy {
initializeMavenDependenciesRetriever()
}
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
private lateinit var testBazelWorkspace: TestBazelWorkspace
- @Rule
- @JvmField
- var tempFolder = TemporaryFolder()
+ @field:[Rule JvmField] val tempFolder = TemporaryFolder()
@Before
fun setUp() {
@@ -99,6 +99,7 @@ class MavenDependenciesRetrieverTest {
@After
fun restoreStreams() {
System.setOut(originalOut)
+ scriptBgDispatcher.close()
}
@Test
@@ -1220,13 +1221,15 @@ class MavenDependenciesRetrieverTest {
}
]
}
- }
+ }
""".trimIndent()
)
}
private fun initializeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl {
- return CommandExecutorImpl(processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES)
+ return CommandExecutorImpl(
+ scriptBgDispatcher, processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES
+ )
}
/** Returns a mock for the [LicenseFetcher]. */
diff --git a/scripts/src/javatests/org/oppia/android/scripts/maven/GenerateMavenDependenciesListTest.kt b/scripts/src/javatests/org/oppia/android/scripts/maven/GenerateMavenDependenciesListTest.kt
index 4507d76a5dc..96e3a92ad64 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/maven/GenerateMavenDependenciesListTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/maven/GenerateMavenDependenciesListTest.kt
@@ -11,6 +11,7 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.oppia.android.scripts.common.CommandExecutorImpl
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.license.LicenseFetcher
import org.oppia.android.scripts.proto.DirectLinkOnly
import org.oppia.android.scripts.proto.ExtractedCopyLink
@@ -64,17 +65,16 @@ class GenerateMavenDependenciesListTest {
private val SCRIPT_PASSED_MESSAGE =
"Script executed succesfully: maven_dependencies.textproto updated successfully."
+ @field:[Rule JvmField] val tempFolder = TemporaryFolder()
+
private val outContent: ByteArrayOutputStream = ByteArrayOutputStream()
private val originalOut: PrintStream = System.out
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
private val mockLicenseFetcher by lazy { initializeLicenseFetcher() }
private val commandExecutor by lazy { initializeCommandExecutorWithLongProcessWaitTime() }
private lateinit var testBazelWorkspace: TestBazelWorkspace
- @Rule
- @JvmField
- var tempFolder = TemporaryFolder()
-
@Before
fun setUp() {
tempFolder.newFolder("scripts", "assets")
@@ -86,6 +86,7 @@ class GenerateMavenDependenciesListTest {
@After
fun restoreStreams() {
System.setOut(originalOut)
+ scriptBgDispatcher.close()
}
@Test
@@ -99,6 +100,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -183,6 +185,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -207,6 +210,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -249,6 +253,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -316,6 +321,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -358,6 +364,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -426,6 +433,7 @@ class GenerateMavenDependenciesListTest {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -519,6 +527,7 @@ class GenerateMavenDependenciesListTest {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -617,6 +626,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -680,6 +690,7 @@ class GenerateMavenDependenciesListTest {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -793,6 +804,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -918,6 +930,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -1031,6 +1044,7 @@ class GenerateMavenDependenciesListTest {
val exception = assertThrows() {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -1150,6 +1164,7 @@ class GenerateMavenDependenciesListTest {
GenerateMavenDependenciesList(
mockLicenseFetcher,
+ scriptBgDispatcher,
commandExecutor
).main(
arrayOf(
@@ -1394,13 +1409,15 @@ class GenerateMavenDependenciesListTest {
}
]
}
- }
+ }
""".trimIndent()
)
}
private fun initializeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl {
- return CommandExecutorImpl(processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES)
+ return CommandExecutorImpl(
+ scriptBgDispatcher, processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES
+ )
}
/** Returns a mock for the [LicenseFetcher]. */
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 a3eb9c825d9..d3075be5f4f 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt
@@ -218,6 +218,11 @@ class RegexPatternValidationCheckTest {
"ActivityScenarioRule can result in order dependence when static state leaks across tests" +
" (such as static module variables), and can make staging much more difficult for platform" +
" parameters. Use ActivityScenario directly, instead."
+ private val referenceComputeIfAbsent =
+ "computeIfAbsent won't desugar and requires Java 8 support (SDK 24+). Suggest using an atomic" +
+ " Kotlin-specific solution, instead."
+ private val cdataShouldNotBeUsed =
+ "CDATA isn't handled by Translatewiki correctly. Use escaped HTML, instead."
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."
@@ -2728,6 +2733,50 @@ class RegexPatternValidationCheckTest {
)
}
+ @Test
+ fun testFileContent_includesReferenceToComputeIfAbsent_fileContentIsNotCorrect() {
+ val prohibitedContent =
+ """
+ someMap.computeIfAbsent(key) { createOtherValue() }
+ """.trimIndent()
+ tempFolder.newFolder("testfiles", "app", "src", "main", "java", "org", "oppia", "android")
+ val stringFilePath = "app/src/main/java/org/oppia/android/TestPresenter.kt"
+ tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent)
+
+ val exception = assertThrows() { runScript() }
+
+ assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR)
+ assertThat(outContent.toString().trim())
+ .isEqualTo(
+ """
+ $stringFilePath:1: $referenceComputeIfAbsent
+ $wikiReferenceNote
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testFileContent_includesCdataContentInStringsXml_fileContentIsNotCorrect() {
+ val prohibitedContent =
+ """
+ Some nested HTML.]]>
+ """.trimIndent()
+ tempFolder.newFolder("testfiles", "app", "src", "main", "res", "values")
+ val stringFilePath = "app/src/main/res/values/strings.xml"
+ tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent)
+
+ val exception = assertThrows() { runScript() }
+
+ assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR)
+ assertThat(outContent.toString().trim())
+ .isEqualTo(
+ """
+ $stringFilePath:1: $cdataShouldNotBeUsed
+ $wikiReferenceNote
+ """.trimIndent()
+ )
+ }
+
/** Runs the regex_pattern_validation_check. */
private fun runScript() {
main(File(tempFolder.root, "testfiles").absolutePath)
diff --git a/scripts/src/javatests/org/oppia/android/scripts/testing/TestGitRepositoryTest.kt b/scripts/src/javatests/org/oppia/android/scripts/testing/TestGitRepositoryTest.kt
index 453f17da044..0782a6c5512 100644
--- a/scripts/src/javatests/org/oppia/android/scripts/testing/TestGitRepositoryTest.kt
+++ b/scripts/src/javatests/org/oppia/android/scripts/testing/TestGitRepositoryTest.kt
@@ -1,12 +1,14 @@
package org.oppia.android.scripts.testing
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.oppia.android.scripts.common.CommandExecutor
import org.oppia.android.scripts.common.CommandExecutorImpl
import org.oppia.android.scripts.common.CommandResult
+import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.testing.assertThrows
import java.io.File
import java.util.UUID
@@ -31,11 +33,15 @@ import java.util.UUID
// Function name: test names are conventionally named with underscores.
@Suppress("FunctionName")
class TestGitRepositoryTest {
- @Rule
- @JvmField
- var tempFolder = TemporaryFolder()
+ @field:[Rule JvmField] val tempFolder = TemporaryFolder()
- private val commandExecutorInterceptor by lazy { CommandExecutorInterceptor() }
+ private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() }
+ private val commandExecutorInterceptor by lazy { CommandExecutorInterceptor(scriptBgDispatcher) }
+
+ @After
+ fun tearDown() {
+ scriptBgDispatcher.close()
+ }
@Test
fun testCreateTestUtility_doesNotImmediatelyCreateAnyFiles() {
@@ -52,7 +58,7 @@ class TestGitRepositoryTest {
testGitRepository.init()
- assertThat(tempFolder.root.list().toList()).containsExactly(".git")
+ assertThat(tempFolder.root.list()?.toList()).containsExactly(".git")
}
@Test
@@ -494,9 +500,11 @@ class TestGitRepositoryTest {
return file
}
- private class CommandExecutorInterceptor : CommandExecutor {
+ private class CommandExecutorInterceptor(
+ scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher
+ ) : CommandExecutor {
private val commandResults = mutableListOf()
- private val realCommandExecutor by lazy { CommandExecutorImpl() }
+ private val realCommandExecutor by lazy { CommandExecutorImpl(scriptBgDispatcher) }
override fun executeCommand(
workingDir: File,
diff --git a/third_party/maven_install.json b/third_party/maven_install.json
index f154369ddb7..453ad22c4d0 100644
--- a/third_party/maven_install.json
+++ b/third_party/maven_install.json
@@ -1,8 +1,8 @@
{
"dependency_tree": {
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
- "__INPUT_ARTIFACTS_HASH": 577686317,
- "__RESOLVED_ARTIFACTS_HASH": 445130734,
+ "__INPUT_ARTIFACTS_HASH": 928649579,
+ "__RESOLVED_ARTIFACTS_HASH": -1902356738,
"conflict_resolution": {
"androidx.constraintlayout:constraintlayout:1.1.3": "androidx.constraintlayout:constraintlayout:2.0.1",
"androidx.core:core:1.0.1": "androidx.core:core:1.3.1",
@@ -1783,12 +1783,12 @@
{
"coord": "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0",
"dependencies": [
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1",
"androidx.lifecycle:lifecycle-common:2.2.0",
"androidx.annotation:annotation:1.1.0",
"androidx.lifecycle:lifecycle-livedata:aar:2.2.0",
"androidx.lifecycle:lifecycle-livedata-core:aar:2.2.0",
"androidx.lifecycle:lifecycle-livedata-core-ktx:aar:2.2.0",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3",
"androidx.arch.core:core-common:2.1.0",
"org.jetbrains.kotlin:kotlin-stdlib:1.5.0",
"androidx.arch.core:core-runtime:aar:2.1.0"
@@ -1797,7 +1797,7 @@
"androidx.lifecycle:lifecycle-livedata:aar:2.2.0",
"androidx.lifecycle:lifecycle-livedata-core-ktx:aar:2.2.0",
"org.jetbrains.kotlin:kotlin-stdlib:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
],
"file": "v1/https/maven.google.com/androidx/lifecycle/lifecycle-livedata-ktx/2.2.0/lifecycle-livedata-ktx-2.2.0.aar",
"mirror_urls": [
@@ -1813,12 +1813,12 @@
{
"coord": "androidx.lifecycle:lifecycle-livedata-ktx:jar:sources:2.2.0",
"dependencies": [
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.1",
"androidx.arch.core:core-runtime:aar:sources:2.1.0",
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
"androidx.lifecycle:lifecycle-livedata-core:aar:sources:2.2.0",
"androidx.lifecycle:lifecycle-livedata:aar:sources:2.2.0",
"androidx.lifecycle:lifecycle-livedata-core-ktx:aar:sources:2.2.0",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.3",
"androidx.annotation:annotation:jar:sources:1.1.0",
"androidx.arch.core:core-common:jar:sources:2.1.0",
"androidx.lifecycle:lifecycle-common:jar:sources:2.2.0"
@@ -1827,7 +1827,7 @@
"androidx.lifecycle:lifecycle-livedata:aar:sources:2.2.0",
"androidx.lifecycle:lifecycle-livedata-core-ktx:aar:sources:2.2.0",
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.1"
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.3"
],
"file": "v1/https/maven.google.com/androidx/lifecycle/lifecycle-livedata-ktx/2.2.0/lifecycle-livedata-ktx-2.2.0-sources.jar",
"mirror_urls": [
@@ -9860,13 +9860,12 @@
{
"coord": "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1",
"dependencies": [
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1",
- "org.jetbrains.kotlin:kotlin-stdlib-common:1.5.0",
- "org.jetbrains.kotlin:kotlin-stdlib:1.5.0"
+ "org.jetbrains.kotlin:kotlin-stdlib:1.5.0",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
],
"directDependencies": [
"org.jetbrains.kotlin:kotlin-stdlib:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
],
"file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.4.1/kotlinx-coroutines-android-1.4.1.jar",
"mirror_urls": [
@@ -9883,12 +9882,11 @@
"coord": "org.jetbrains.kotlinx:kotlinx-coroutines-android:jar:sources:1.4.1",
"dependencies": [
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.1",
- "org.jetbrains.kotlin:kotlin-stdlib-common:jar:sources:1.5.0"
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.3"
],
"directDependencies": [
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.1"
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.3"
],
"file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.4.1/kotlinx-coroutines-android-1.4.1-sources.jar",
"mirror_urls": [
@@ -9902,7 +9900,7 @@
"url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-android/1.4.1/kotlinx-coroutines-android-1.4.1-sources.jar"
},
{
- "coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1",
+ "coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.3",
"dependencies": [
"org.jetbrains.kotlin:kotlin-stdlib-common:1.5.0",
"org.jetbrains.kotlin:kotlin-stdlib:1.5.0"
@@ -9911,19 +9909,19 @@
"org.jetbrains.kotlin:kotlin-stdlib:1.5.0",
"org.jetbrains.kotlin:kotlin-stdlib-common:1.5.0"
],
- "file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.jar",
+ "file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3.jar",
"mirror_urls": [
- "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.jar",
- "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.jar",
- "https://maven.fabric.io/public/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.jar",
- "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.jar",
- "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.jar"
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3.jar",
+ "https://maven.fabric.io/public/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3.jar",
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3.jar"
],
- "sha256": "6d2f87764b6638f27aff12ed380db4b63c9d46ba55dc32683a650598fa5a3e22",
- "url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1.jar"
+ "sha256": "f7be08ddf86bd88020da7b78adbf44228799cca54d5c0c4396d850bc66725163",
+ "url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3.jar"
},
{
- "coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.1",
+ "coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:jar:sources:1.4.3",
"dependencies": [
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
"org.jetbrains.kotlin:kotlin-stdlib-common:jar:sources:1.5.0"
@@ -9932,16 +9930,58 @@
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
"org.jetbrains.kotlin:kotlin-stdlib-common:jar:sources:1.5.0"
],
- "file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1-sources.jar",
+ "file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3-sources.jar",
+ "mirror_urls": [
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3-sources.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3-sources.jar",
+ "https://maven.fabric.io/public/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3-sources.jar",
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3-sources.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3-sources.jar"
+ ],
+ "sha256": "6f1a3f8be952a3c7c003cb32eca36a92a7afc4affea6cd8b769dd23d7f14bad2",
+ "url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.4.3/kotlinx-coroutines-core-jvm-1.4.3-sources.jar"
+ },
+ {
+ "coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3",
+ "dependencies": [
+ "org.jetbrains.kotlin:kotlin-stdlib-common:1.5.0",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.3",
+ "org.jetbrains.kotlin:kotlin-stdlib:1.5.0"
+ ],
+ "directDependencies": [
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.3"
+ ],
+ "file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3.jar",
+ "mirror_urls": [
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3.jar",
+ "https://maven.fabric.io/public/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3.jar",
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3.jar"
+ ],
+ "sha256": "de487d57b156e4e237abbc9cf7fff8777b2495aff6caa8bc4e9cf6ec859f0224",
+ "url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3.jar"
+ },
+ {
+ "coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.3",
+ "dependencies": [
+ "org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:jar:sources:1.4.3",
+ "org.jetbrains.kotlin:kotlin-stdlib-common:jar:sources:1.5.0"
+ ],
+ "directDependencies": [
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:jar:sources:1.4.3"
+ ],
+ "file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3-sources.jar",
"mirror_urls": [
- "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1-sources.jar",
- "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1-sources.jar",
- "https://maven.fabric.io/public/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1-sources.jar",
- "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1-sources.jar",
- "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1-sources.jar"
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3-sources.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3-sources.jar",
+ "https://maven.fabric.io/public/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3-sources.jar",
+ "https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3-sources.jar",
+ "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3-sources.jar"
],
- "sha256": "bb339efebc2d9141401f1aa43a035abe929210e362cfff13d03c6b7b11dc0469",
- "url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.1/kotlinx-coroutines-core-1.4.1-sources.jar"
+ "sha256": "315d99f5b340ceaba2cb898eff7e6414fd615b27e3f093b84ac13ac31c2f7dc0",
+ "url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.4.3/kotlinx-coroutines-core-1.4.3-sources.jar"
},
{
"coord": "org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.2.2",
@@ -9976,13 +10016,13 @@
{
"coord": "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2",
"dependencies": [
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1",
"org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.2.2",
- "org.jetbrains.kotlin:kotlin-stdlib:1.5.0"
+ "org.jetbrains.kotlin:kotlin-stdlib:1.5.0",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
],
"directDependencies": [
"org.jetbrains.kotlin:kotlin-stdlib:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3",
"org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.2.2"
],
"file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-test/1.2.2/kotlinx-coroutines-test-1.2.2.jar",
@@ -10000,12 +10040,12 @@
"coord": "org.jetbrains.kotlinx:kotlinx-coroutines-test:jar:sources:1.2.2",
"dependencies": [
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.1",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.3",
"org.jetbrains.kotlinx:kotlinx-coroutines-debug:jar:sources:1.2.2"
],
"directDependencies": [
"org.jetbrains.kotlin:kotlin-stdlib:jar:sources:1.5.0",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.1",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:jar:sources:1.4.3",
"org.jetbrains.kotlinx:kotlinx-coroutines-debug:jar:sources:1.2.2"
],
"file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-test/1.2.2/kotlinx-coroutines-test-1.2.2-sources.jar",
diff --git a/third_party/versions.bzl b/third_party/versions.bzl
index f62c87f8f15..499494bea41 100644
--- a/third_party/versions.bzl
+++ b/third_party/versions.bzl
@@ -79,7 +79,7 @@ MAVEN_PRODUCTION_DEPENDENCY_VERSIONS = {
"org.checkerframework:checker-qual": "3.13.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar": "1.3.72",
"org.jetbrains.kotlinx:kotlinx-coroutines-android": "1.4.1",
- "org.jetbrains.kotlinx:kotlinx-coroutines-core": "1.4.1",
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core": "1.4.3",
"org.jetbrains:annotations:jar": "13.0",
}
diff --git a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt
index 93049848c3c..4c11876168e 100644
--- a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt
+++ b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
+import org.oppia.android.util.data.DataProviders.Companion.transform
import org.oppia.android.util.logging.ExceptionLogger
import org.oppia.android.util.threading.BackgroundDispatcher
import java.util.concurrent.atomic.AtomicBoolean
@@ -74,7 +75,12 @@ class DataProviders @Inject constructor(
override fun getId(): Any = newId
override suspend fun retrieveData(): AsyncResult {
- return this@transformAsync.retrieveData().transformAsync(function)
+ return try {
+ this@transformAsync.retrieveData().transformAsync(function)
+ } catch (e: Exception) {
+ dataProviders.exceptionLogger.logException(e)
+ AsyncResult.Failure(e)
+ }
}
}
}
diff --git a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt
index ffe3e186646..e871355442b 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt
+++ b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleFactory.kt
@@ -1,6 +1,10 @@
package org.oppia.android.util.locale
import android.os.Build
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
import org.oppia.android.app.model.LanguageSupportDefinition
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId
import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.LanguageTypeCase.IETF_BCP47_ID
@@ -9,8 +13,8 @@ import org.oppia.android.app.model.LanguageSupportDefinition.LanguageId.Language
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.util.threading.BlockingDispatcher
import java.util.Locale
-import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
@@ -20,9 +24,28 @@ import javax.inject.Singleton
*/
@Singleton
class AndroidLocaleFactory @Inject constructor(
- private val profileChooserSelector: ProposalChooser.Selector
+ private val profileChooserSelector: ProposalChooser.Selector,
+ @BlockingDispatcher private val blockingDispatcher: CoroutineDispatcher,
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
) {
- private val memoizedLocales by lazy { ConcurrentHashMap() }
+ private val memoizedLocales = mutableMapOf()
+
+ /**
+ * Creates and returns a new [Locale] that matches the given [OppiaLocaleContext].
+ *
+ * See [createAndroidLocaleAsync] for specifics. Note that this function, unlike the async
+ * version, does **not** cache or try to load a pre-created [Locale] for the given context.
+ * Creating new [Locale]s can be expensive, so it's always preferred to use
+ * [createAndroidLocaleAsync] except in cases where that isn't an option.
+ */
+ fun createOneOffAndroidLocale(localeContext: OppiaLocaleContext): Locale {
+ val chooser = profileChooserSelector.findBestChooser(localeContext)
+ val primaryLocaleSource =
+ LocaleSource.createFromPrimary(localeContext, androidLocaleProfileFactory)
+ val fallbackLocaleSource =
+ LocaleSource.createFromFallback(localeContext, androidLocaleProfileFactory)
+ return chooser.findBestProposal(primaryLocaleSource, fallbackLocaleSource).computedLocale
+ }
/**
* Creates a new [Locale] that matches the given [OppiaLocaleContext].
@@ -50,18 +73,17 @@ class AndroidLocaleFactory @Inject constructor(
* - For other locale-based operations, the forced [Locale] will behave like the system's
* [Locale.ROOT].
*
+ * Note that the returned [Locale] may be cached within the factory for performance reasons, so
+ * the returned value uses a [Deferred] to ensure that this method can guarantee thread-safe
+ * access.
+ *
* @param localeContext the [OppiaLocaleContext] to use as a basis for finding a similar [Locale]
* @return the best [Locale] to match the provided [localeContext]
*/
- fun createAndroidLocale(localeContext: OppiaLocaleContext): Locale {
- // Note: computeIfAbsent is used here instead of getOrPut to ensure atomicity across multiple
- // threads calling into this create function.
- return memoizedLocales.computeIfAbsent(localeContext) {
- val chooser = profileChooserSelector.findBestChooser(localeContext)
- val primaryLocaleSource = LocaleSource.createFromPrimary(localeContext)
- val fallbackLocaleSource = LocaleSource.createFromFallback(localeContext)
- val proposal = chooser.findBestProposal(primaryLocaleSource, fallbackLocaleSource)
- return@computeIfAbsent proposal.computedLocale
+ fun createAndroidLocaleAsync(localeContext: OppiaLocaleContext): Deferred {
+ // A blocking dispatcher is used to ensure thread safety when updating the locales map.
+ return CoroutineScope(blockingDispatcher).async {
+ memoizedLocales.getOrPut(localeContext) { createOneOffAndroidLocale(localeContext) }
}
}
@@ -76,21 +98,17 @@ class AndroidLocaleFactory @Inject constructor(
/**
* A computed [Locale] that most closely represents the [AndroidLocaleProfile] of this proposal.
*/
- val computedLocale: Locale
- get() = Locale(profile.languageCode, profile.getNonWildcardRegionCode())
+ val computedLocale: Locale by lazy { profile.computeAndroidLocale() }
/**
* Determines whether the [AndroidLocaleProfile] of this proposal is a viable choice for using
* to compute a [Locale] (e.g. via [computedLocale]).
*
- * @param machineLocale the app's [OppiaLocale.MachineLocale]
- * @param systemProfiles [AndroidLocaleProfile]s representing the system's available locales
+ * @param localeProfileRepository the [LocaleProfileRepository]s representing the system's
+ * available locales
* @return whether this proposal has a viable profile for creating a [Locale]
*/
- abstract fun isViable(
- machineLocale: OppiaLocale.MachineLocale,
- systemProfiles: List
- ): Boolean
+ abstract fun isViable(localeProfileRepository: LocaleProfileRepository): Boolean
/**
* A [LocaleProfileProposal] that is only viable if its [profile] is among the available system
@@ -101,10 +119,9 @@ class AndroidLocaleFactory @Inject constructor(
val minAndroidSdkVersion: Int
) : LocaleProfileProposal() {
override fun isViable(
- machineLocale: OppiaLocale.MachineLocale,
- systemProfiles: List
+ localeProfileRepository: LocaleProfileRepository
): Boolean {
- return systemProfiles.any { it.matches(machineLocale, profile) } &&
+ return localeProfileRepository.availableLocaleProfiles.any { it.matches(profile) } &&
minAndroidSdkVersion <= Build.VERSION.SDK_INT
}
}
@@ -119,15 +136,8 @@ class AndroidLocaleFactory @Inject constructor(
override val profile: AndroidLocaleProfile,
val minAndroidSdkVersion: Int
) : LocaleProfileProposal() {
- override fun isViable(
- machineLocale: OppiaLocale.MachineLocale,
- systemProfiles: List
- ): Boolean = minAndroidSdkVersion <= Build.VERSION.SDK_INT
- }
-
- private companion object {
- private fun AndroidLocaleProfile.getNonWildcardRegionCode(): String =
- regionCode.takeIf { it != AndroidLocaleProfile.REGION_WILDCARD } ?: ""
+ override fun isViable(localeProfileRepository: LocaleProfileRepository): Boolean =
+ minAndroidSdkVersion <= Build.VERSION.SDK_INT
}
}
@@ -143,7 +153,8 @@ class AndroidLocaleFactory @Inject constructor(
class LocaleSource private constructor(
private val localeContext: OppiaLocaleContext,
private val definition: LanguageSupportDefinition,
- private val languageId: LanguageId
+ private val languageId: LanguageId,
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
) {
private val regionDefinition by lazy {
localeContext.regionDefinition.takeIf { localeContext.hasRegionDefinition() }
@@ -156,7 +167,7 @@ class AndroidLocaleFactory @Inject constructor(
*/
fun computeSystemMatchingProposals(): List {
return listOfNotNull(
- computeLocaleProfileFromAndroidId()?.toSystemProposal(),
+ createAndroidResourcesProfile()?.toSystemProposal(),
createIetfProfile()?.toSystemProposal(),
createMacaronicProfile()?.toSystemProposal()
)
@@ -169,7 +180,7 @@ class AndroidLocaleFactory @Inject constructor(
* configured for this source's context.
*/
fun computeForcedAndroidProposal(): LocaleProfileProposal? =
- computeLocaleProfileFromAndroidId()?.toForcedProposal()
+ createAndroidResourcesProfile()?.toForcedProposal()
/**
* Returns a [LocaleProfileProposal] representing a [LocaleProfileProposal.ForcedProposal] that
@@ -177,37 +188,33 @@ class AndroidLocaleFactory @Inject constructor(
*
* Note that the returned proposal will prioritize its Android ID configuration over
* alternatives (such as IETF BCP 47 or a macaronic language configuration).
+ *
+ * @param fallBackToRootProfile whether to return a [AndroidLocaleProfile.RootProfile] for cases
+ * when a valid proposal cannot be determined rather than throwing an exception
*/
- fun computeForcedProposal(): LocaleProfileProposal =
- computeForcedAndroidProposal() ?: languageId.toForcedProposal()
-
- private fun computeLocaleProfileFromAndroidId(): AndroidLocaleProfile? {
- return languageId.androidResourcesLanguageId.takeIf {
- languageId.hasAndroidResourcesLanguageId() && it.languageCode.isNotEmpty()
- }?.let {
- // 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).
- AndroidLocaleProfile(
- it.languageCode,
- regionCode = it.regionCode.ifEmpty { AndroidLocaleProfile.REGION_WILDCARD }
- )
- }
- }
+ fun computeForcedProposal(fallBackToRootProfile: Boolean): LocaleProfileProposal =
+ computeForcedAndroidProposal() ?: toForcedProposal(fallBackToRootProfile)
- private fun LanguageId.toForcedProposal(): LocaleProfileProposal {
- return when (languageId.languageTypeCase) {
+ private fun toForcedProposal(fallBackToRootProfile: Boolean): LocaleProfileProposal {
+ return when (val languageTypeCase = languageId.languageTypeCase) {
IETF_BCP47_ID -> createIetfProfile().expectedProfile()
MACARONIC_ID -> createMacaronicProfile().expectedProfile()
- LANGUAGETYPE_NOT_SET, null -> error("Invalid language case: $languageTypeCase.")
+ LANGUAGETYPE_NOT_SET, null -> {
+ if (fallBackToRootProfile) {
+ AndroidLocaleProfile.RootProfile
+ } else error("Invalid language case: $languageTypeCase.")
+ }
}.toForcedProposal()
}
private fun createIetfProfile(): AndroidLocaleProfile? =
- AndroidLocaleProfile.createFromIetfDefinitions(languageId, regionDefinition)
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageId, regionDefinition)
private fun createMacaronicProfile(): AndroidLocaleProfile? =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageId)
+ androidLocaleProfileFactory.createFromMacaronicLanguage(languageId)
+
+ private fun createAndroidResourcesProfile(): AndroidLocaleProfile? =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(languageId)
private fun AndroidLocaleProfile?.expectedProfile() = this ?: error("Invalid ID: $languageId.")
@@ -222,18 +229,31 @@ class AndroidLocaleFactory @Inject constructor(
* Return a new [LocaleSource] that maps to [localeContext]'s primary language configuration
* (i.e. fallback language details will be ignored).
*/
- fun createFromPrimary(localeContext: OppiaLocaleContext): LocaleSource =
- LocaleSource(localeContext, localeContext.languageDefinition, localeContext.getLanguageId())
+ fun createFromPrimary(
+ localeContext: OppiaLocaleContext,
+ androidLocaleProfileFactory: AndroidLocaleProfile.Factory
+ ): LocaleSource {
+ return LocaleSource(
+ localeContext,
+ localeContext.languageDefinition,
+ localeContext.getLanguageId(),
+ androidLocaleProfileFactory
+ )
+ }
/**
* Return a new [LocaleSource] that maps to [localeContext]'s fallback (secondary) language
* configuration (i.e. primary language details will be ignored).
*/
- fun createFromFallback(localeContext: OppiaLocaleContext): LocaleSource {
+ fun createFromFallback(
+ localeContext: OppiaLocaleContext,
+ androidLocaleProfileFactory: AndroidLocaleProfile.Factory
+ ): LocaleSource {
return LocaleSource(
localeContext,
localeContext.fallbackLanguageDefinition,
- localeContext.getFallbackLanguageId()
+ localeContext.getFallbackLanguageId(),
+ androidLocaleProfileFactory
)
}
}
@@ -288,15 +308,15 @@ class AndroidLocaleFactory @Inject constructor(
* system locales.
*/
class MatchedLocalePreferredChooser @Inject constructor(
- private val machineLocale: OppiaLocale.MachineLocale
+ private val localeProfileRepository: LocaleProfileRepository
) : ProposalChooser {
override fun findBestProposal(
primarySource: LocaleSource,
fallbackSource: LocaleSource
): LocaleProfileProposal {
- return primarySource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: primarySource.computeForcedProposal()
+ return primarySource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: primarySource.computeForcedProposal(fallBackToRootProfile = false)
}
}
@@ -308,31 +328,43 @@ class AndroidLocaleFactory @Inject constructor(
* [Locale]s produced by such profiles in order to correctly produce app UI strings.
*/
class AndroidResourceCompatibilityPreferredChooser @Inject constructor(
- private val machineLocale: OppiaLocale.MachineLocale
+ private val localeProfileRepository: LocaleProfileRepository
) : ProposalChooser {
override fun findBestProposal(
primarySource: LocaleSource,
fallbackSource: LocaleSource
): LocaleProfileProposal {
- return primarySource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: primarySource.computeForcedAndroidProposal()?.takeOnlyIfViable(machineLocale)
- ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(machineLocale)
- ?: fallbackSource.computeForcedAndroidProposal()?.takeOnlyIfViable(machineLocale)
- ?: primarySource.computeForcedProposal()
+ // Note that defaulting to the root locale only makes sense for app strings (since app strings
+ // are picked based on the configured system locale).
+ return primarySource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: primarySource.computeForcedAndroidProposal()?.takeOnlyIfViable(localeProfileRepository)
+ ?: fallbackSource.computeSystemMatchingProposals().findFirstViable(localeProfileRepository)
+ ?: fallbackSource.computeForcedAndroidProposal()?.takeOnlyIfViable(localeProfileRepository)
+ ?: primarySource.computeForcedProposal(fallBackToRootProfile = true)
}
}
- private companion object {
- private val availableLocaleProfiles by lazy {
- Locale.getAvailableLocales().map(AndroidLocaleProfile::createFrom)
+ /**
+ * An application-injectable repository storing all possible [AndroidLocaleProfile]s available to
+ * use on the local system for the lifetime of the current app instance.
+ */
+ @Singleton
+ class LocaleProfileRepository @Inject constructor(
+ private val androidLocaleProfileFactory: AndroidLocaleProfile.Factory
+ ) {
+ /**
+ * All available [AndroidLocaleProfile]s that represent locales on the current running system.
+ */
+ val availableLocaleProfiles: List by lazy {
+ Locale.getAvailableLocales().map { androidLocaleProfileFactory.createFrom(it) }
}
+ }
- private fun List.findFirstViable(
- machineLocale: OppiaLocale.MachineLocale
- ) = firstOrNull { it.isViable(machineLocale, availableLocaleProfiles) }
+ private companion object {
+ private fun List.findFirstViable(repository: LocaleProfileRepository) =
+ firstOrNull { it.isViable(repository) }
- private fun LocaleProfileProposal.takeOnlyIfViable(
- machineLocale: OppiaLocale.MachineLocale
- ): LocaleProfileProposal? = takeIf { isViable(machineLocale, availableLocaleProfiles) }
+ private fun LocaleProfileProposal.takeOnlyIfViable(repository: LocaleProfileRepository) =
+ takeIf { isViable(repository) }
}
}
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
index a410c2b06b6..9c96ec0f8fc 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt
+++ b/utility/src/main/java/org/oppia/android/util/locale/AndroidLocaleProfile.kt
@@ -3,50 +3,156 @@ 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
+import javax.inject.Inject
/**
* 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/3 language code
- * @property regionCode the IETF BCP 47 or ISO 3166 alpha-2 region code
+ * Subclasses of this sealed class operate on a language code and/or region code. The language code
+ * is an IETF BCP 47 or ISO 639-2/3 language code, or empty if unknown or not specified. The region
+ * code is an IETF BCP 47 or ISO 3166 alpha-2 region code, or empty if unknown or not specified.
+ *
+ * New instances should be created using [Factory].
*/
-data class AndroidLocaleProfile(val languageCode: String, val regionCode: String) {
+sealed class AndroidLocaleProfile {
+ /**
+ * An IETF BCP 47-esque language tag that represents this locale profile. For profiles that have
+ * valid IETF BCP 47 language & region codes, the returned tag should be a valid IETF BCP 47
+ * language tag.
+ */
+ abstract val ietfLanguageTag: 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
+ abstract fun matches(otherProfile: AndroidLocaleProfile): Boolean
+
+ /** Returns an Android [Locale] compatible with this profile. */
+ abstract fun computeAndroidLocale(): Locale
+
+ /**
+ * An [AndroidLocaleProfile] that provides both a non-empty language and region code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property languageCode the lowercase two-letter language code in this profile
+ * @property regionCode the lowercase two-letter region code in this profile
+ * @property regionCodeUpperCase the uppercase version of [regionCode]
+ */
+ data class LanguageAndRegionProfile(
+ val languageCode: String,
+ val regionCode: String,
+ private val regionCodeUpperCase: String
+ ) : AndroidLocaleProfile() {
+ // The region code is usually uppercase in IETF BCP-47 tags when extending a language code.
+ override val ietfLanguageTag = "$languageCode-$regionCodeUpperCase"
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean {
+ return when (otherProfile) {
+ is LanguageAndRegionProfile ->
+ languageCode == otherProfile.languageCode && regionCode == otherProfile.regionCode
+ is LanguageAndWildcardRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageOnlyProfile, is RegionOnlyProfile, is RootProfile -> false
+ }
}
+
+ override fun computeAndroidLocale(): Locale = Locale(languageCode, regionCode)
}
/**
- * Returns an IETF BCP 47-esque language tag that represents this locale profile. For profiles
- * that have valid IETF BCP 47 language & region codes, the returned tag should be a valid IETF
- * BCP 47 language tag.
+ * An [AndroidLocaleProfile] that provides only a non-empty region code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property regionCode the lowercase two-letter region code in this profile
*/
- fun computeIetfLanguageTag(): String {
- return when {
- languageCode.isNotEmpty() && regionCode.isNotEmptyOrWildcard() -> "$languageCode-$regionCode"
- regionCode.isNotEmptyOrWildcard() -> regionCode
- else -> languageCode
+ data class RegionOnlyProfile(val regionCode: String) : AndroidLocaleProfile() {
+ override val ietfLanguageTag = regionCode
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean =
+ otherProfile is RegionOnlyProfile && regionCode == otherProfile.regionCode
+
+ override fun computeAndroidLocale(): Locale = Locale(/* language = */ "", regionCode)
+ }
+
+ /**
+ * An [AndroidLocaleProfile] that provides only a non-empty language code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property languageCode the lowercase two-letter language code in this profile
+ */
+ data class LanguageOnlyProfile(val languageCode: String) : AndroidLocaleProfile() {
+ override val ietfLanguageTag = languageCode
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean {
+ return when (otherProfile) {
+ is LanguageOnlyProfile -> languageCode == otherProfile.languageCode
+ is LanguageAndWildcardRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageAndRegionProfile, is RegionOnlyProfile, is RootProfile -> false
+ }
+ }
+
+ override fun computeAndroidLocale(): Locale = Locale(languageCode)
+ }
+
+ /**
+ * An [AndroidLocaleProfile] that provides only a non-empty language code, but matches (e.g. via
+ * [matches]) with any profile that has the same language code.
+ *
+ * Note that, generally, this should never need to be created directly. Instead, [Factory] should
+ * be used to create new instances of profiles.
+ *
+ * @property languageCode the lowercase two-letter language code in this profile
+ */
+ data class LanguageAndWildcardRegionProfile(val languageCode: String) : AndroidLocaleProfile() {
+ override val ietfLanguageTag = languageCode
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean {
+ return when (otherProfile) {
+ is LanguageAndRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageAndWildcardRegionProfile -> languageCode == otherProfile.languageCode
+ is LanguageOnlyProfile -> languageCode == otherProfile.languageCode
+ is RegionOnlyProfile, is RootProfile -> false
+ }
}
+
+ override fun computeAndroidLocale(): Locale = Locale(languageCode)
}
- companion object {
- /** A wildcard that will match against any region when provided. */
- const val REGION_WILDCARD = "*"
+ /**
+ * An [AndroidLocaleProfile] that provides the system's root locale ([Locale.ROOT]).
+ *
+ * Note that, generally, this should never need to be used directly. Instead, [Factory] should be
+ * used to create new instances of profiles.
+ */
+ object RootProfile : AndroidLocaleProfile() {
+ override val ietfLanguageTag = ""
+
+ override fun matches(otherProfile: AndroidLocaleProfile): Boolean = otherProfile is RootProfile
+ override fun computeAndroidLocale(): Locale = Locale.ROOT
+ }
+
+ /** An application-injectable factory for creating new [AndroidLocaleProfile]s. */
+ class Factory @Inject constructor(private val machineLocale: OppiaLocale.MachineLocale) {
/** Returns a new [AndroidLocaleProfile] that represents the specified Android [Locale]. */
- fun createFrom(androidLocale: Locale): AndroidLocaleProfile =
- AndroidLocaleProfile(androidLocale.language, androidLocale.country)
+ fun createFrom(androidLocale: Locale): AndroidLocaleProfile {
+ val languageCode = androidLocale.language
+ val regionCode = androidLocale.country
+ return when {
+ languageCode.isNotEmpty() && regionCode.isNotEmpty() -> {
+ LanguageAndRegionProfile(
+ languageCode.asLowerCase(), regionCode.asLowerCase(), regionCode.asUpperCase()
+ )
+ }
+ regionCode.isNotEmpty() -> RegionOnlyProfile(regionCode.asLowerCase())
+ languageCode.isNotEmpty() -> LanguageOnlyProfile(languageCode.asLowerCase())
+ else -> RootProfile
+ }
+ }
/**
* Returns a new [AndroidLocaleProfile] using the IETF BCP 47 tag in the provided [LanguageId].
@@ -100,17 +206,48 @@ data class AndroidLocaleProfile(val languageCode: String, val regionCode: String
return maybeConstructProfile(languageCode, regionCode)
}
+ /**
+ * Returns a new [AndroidLocaleProfile] using the provided [languageId]'s
+ * [LanguageId.getAndroidResourcesLanguageId] as the basis of the profile, or null if none can
+ * be created.
+ *
+ * This is meant to be used in cases when an [AndroidLocaleProfile] is needed to match a
+ * specific Android-compatible [Locale] (e.g. via [AndroidLocaleProfile.computeAndroidLocale])
+ * that can correctly match to specific Android app strings.
+ */
+ fun createFromAndroidResourcesLanguageId(languageId: LanguageId): AndroidLocaleProfile? {
+ val languageCode = languageId.androidResourcesLanguageId.languageCode
+ val regionCode = languageId.androidResourcesLanguageId.regionCode
+ return when {
+ !languageId.hasAndroidResourcesLanguageId() -> null
+ languageCode.isEmpty() -> null
+ // 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).
+ regionCode.isEmpty() -> LanguageAndWildcardRegionProfile(languageCode.asLowerCase())
+ else -> {
+ LanguageAndRegionProfile(
+ languageCode.asLowerCase(), regionCode.asLowerCase(), regionCode.asUpperCase()
+ )
+ }
+ }
+ }
+
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
+ return when {
+ languageCode.isEmpty() -> null
+ regionCode.isNotEmpty() -> {
+ LanguageAndRegionProfile(
+ languageCode.asLowerCase(), regionCode.asLowerCase(), regionCode.asUpperCase()
+ )
+ }
+ emptyRegionAsWildcard -> LanguageAndWildcardRegionProfile(languageCode.asLowerCase())
+ else -> null
+ }
}
private fun String.divide(delimiter: String): Pair? {
@@ -120,6 +257,8 @@ data class AndroidLocaleProfile(val languageCode: String, val regionCode: String
} else null
}
- private fun String.isNotEmptyOrWildcard() = isNotEmpty() && this != REGION_WILDCARD
+ private fun String.asLowerCase() = machineLocale.run { toMachineLowerCase() }
+
+ private fun String.asUpperCase() = machineLocale.run { toMachineUpperCase() }
}
}
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
index dcec5fbc2c9..9635257d80e 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel
+++ b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel
@@ -12,6 +12,7 @@ kt_android_library(
],
visibility = ["//:oppia_api_visibility"],
deps = [
+ ":dagger",
":oppia_locale",
],
)
@@ -75,6 +76,8 @@ kt_android_library(
deps = [
":android_locale_profile",
":dagger",
+ "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
+ "//utility/src/main/java/org/oppia/android/util/threading:annotations",
],
)
diff --git a/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt b/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt
index 838c8b192f7..f8bff8b2b06 100644
--- a/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt
+++ b/utility/src/main/java/org/oppia/android/util/locale/DisplayLocaleImpl.kt
@@ -12,17 +12,21 @@ import java.util.Date
import java.util.Locale
import java.util.Objects
-// TODO(#3766): Restrict to be 'internal'.
-/** Implementation of [OppiaLocale.DisplayLocale]. */
+// TODO(#3766): Restrict DisplayLocaleImpl and formattingLocale to be 'internal'.
+/**
+ * Implementation of [OppiaLocale.DisplayLocale].
+ *
+ * @property localeContext the [OppiaLocaleContext] that this locale is representing
+ * @property formattingLocale the [Locale] used for user-facing string formatting
+ * @property machineLocale the application-wide [MachineLocale] used for string formatting
+ * @property formatterFactory the application-wide factory for creating a new [OppiaBidiFormatter]
+ */
class DisplayLocaleImpl(
localeContext: OppiaLocaleContext,
+ val formattingLocale: Locale,
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)
}
diff --git a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt
index d4697e891b7..596b43e2334 100644
--- a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt
@@ -972,6 +972,40 @@ class DataProvidersTest {
}
}
+ @Test
+ fun testTransformAsync_toLiveData_throwsException_deliversFailure() {
+ val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0)
+ val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) {
+ throw IllegalStateException("Transform failure")
+ }
+
+ dataProvider.toLiveData().observeForever(mockIntLiveDataObserver)
+ testCoroutineDispatchers.advanceUntilIdle()
+
+ // Note that the exception type here is not chained since the failure occurred in the transform
+ // function.
+ verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture())
+ assertThat(intResultCaptor.value).isFailureThat().apply {
+ isInstanceOf(IllegalStateException::class.java)
+ hasMessageThat().contains("Transform failure")
+ }
+ }
+
+ @Test
+ fun testTransformAsync_toLiveData_throwsException_deliversFailure_logsException() {
+ val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0)
+ val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) {
+ throw IllegalStateException("Transform failure")
+ }
+
+ dataProvider.toLiveData().observeForever(mockIntLiveDataObserver)
+ testCoroutineDispatchers.advanceUntilIdle()
+ val exception = fakeExceptionLogger.getMostRecentException()
+
+ assertThat(exception).isInstanceOf(IllegalStateException::class.java)
+ assertThat(exception).hasMessageThat().contains("Transform failure")
+ }
+
@Test
fun testTransformAsync_toLiveData_basePending_deliversPending() {
val baseProvider = createPendingDataProvider(BASE_PROVIDER_ID_0)
diff --git a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt
index c009baad4e0..581668c878c 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleFactoryTest.kt
@@ -9,6 +9,10 @@ import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -22,9 +26,14 @@ 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.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.threading.BackgroundDispatcher
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -41,18 +50,63 @@ import javax.inject.Singleton
@LooperMode(LooperMode.Mode.PAUSED)
@Config(manifest = Config.NONE)
class AndroidLocaleFactoryTest {
- @Inject
- lateinit var androidLocaleFactory: AndroidLocaleFactory
+ @Inject lateinit var androidLocaleFactory: AndroidLocaleFactory
+ @field:[Inject BackgroundDispatcher] lateinit var backgroundDispatcher: CoroutineDispatcher
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
@Before
fun setUp() {
setUpTestApplicationComponent()
}
+ /* Basic tests for one-off Locale creation (latter functions indirectly test in more detail. */
+
+ @Test
+ fun testCreateOneOffAndroidLocale_default_throwsException() {
+ val exception = assertThrows() {
+ androidLocaleFactory.createOneOffAndroidLocale(OppiaLocaleContext.getDefaultInstance())
+ }
+
+ // The operation should fail since there's no language type defined.
+ assertThat(exception).hasMessageThat().contains("Invalid language case")
+ }
+
+ @Test
+ fun testCreateOneOffAndroidLocale_appStrings_defaultLanguage_returnsRootLocale() {
+ val context =
+ createAppStringsContext(
+ language = OppiaLanguage.LANGUAGE_UNSPECIFIED,
+ appStringId = LanguageId.getDefaultInstance(),
+ regionDefinition = RegionSupportDefinition.getDefaultInstance()
+ )
+
+ val locale = androidLocaleFactory.createOneOffAndroidLocale(context)
+
+ assertThat(locale).isEqualTo(Locale.ROOT)
+ }
+
+ @Test
+ fun testCreateOneOffAndroidLocale_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.createOneOffAndroidLocale(context)
+
+ // The context should be matched to a valid locale.
+ assertThat(locale.language).isEqualTo("pt")
+ assertThat(locale.country).isEqualTo("BR")
+ }
+
+ /* Begin createAndroidLocaleAsync tests. */
+
@Test
fun testCreateLocale_default_throwsException() {
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(OppiaLocaleContext.getDefaultInstance())
+ androidLocaleFactory.createAndroidLocaleBlocking(OppiaLocaleContext.getDefaultInstance())
}
// The operation should fail since there's no language type defined.
@@ -70,7 +124,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -86,7 +140,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -102,7 +156,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_US
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale. Note that BR is matched since the IETF
// language tag includes the region.
@@ -119,7 +173,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -138,7 +192,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -158,7 +212,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -176,7 +230,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked because it's an Android ID for app strings, so it's always taken as a forced
// locale over any fallback options.
@@ -194,7 +248,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale, and it's not
// an Android ID that would take precedence.
@@ -212,7 +266,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -229,7 +283,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language's region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -246,7 +300,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_ZZ
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the supplied region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -263,7 +317,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -280,7 +334,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language is invalid.
assertThat(locale.language).isEqualTo("pt")
@@ -298,7 +352,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language isn't compatible with the current SDK.
assertThat(locale.language).isEqualTo("pt")
@@ -318,7 +372,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked because it's an Android ID for app strings, so it's always taken as a forced
// locale over any fallback options.
@@ -339,7 +393,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale, and it's not
// an Android ID that would take precedence. Beyond that, the fallback's Android ID should take
@@ -361,7 +415,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked because it's an Android ID for app strings, so it's always taken as a forced
// locale over any fallback options.
@@ -382,7 +436,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options, and
// none of the primary options are viable.
@@ -400,7 +454,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is picked over the primary language because it's an Android ID and the primary language
// doesn't match any locales.
@@ -418,7 +472,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is the exact locale being requested.
assertThat(locale.language).isEqualTo("qq")
@@ -438,7 +492,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the IETF language since Android IDs are picked first when
// creating a forced locale.
@@ -459,7 +513,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the macaronic language since Android IDs are picked first when
// creating a forced locale.
@@ -477,7 +531,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The IETF language ID is used for the forced locale (note that fallback languages are ignored
// when computing the forced locale).
@@ -495,7 +549,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Hinglish macaronic language ID is used for the forced locale (note that fallback
// languages are ignored when computing the forced locale).
@@ -514,7 +568,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid ID")
@@ -532,7 +586,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(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).
@@ -549,7 +603,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(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).
@@ -558,7 +612,7 @@ class AndroidLocaleFactoryTest {
}
@Test
- fun testCreateLocale_appStrings_allIncompat_invalidLangType_throwsException() {
+ fun testCreateLocale_appStrings_allIncompat_invalidLangType_returnsRootLocale() {
val context =
createAppStringsContext(
language = OppiaLanguage.ENGLISH,
@@ -566,11 +620,9 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
- }
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
- assertThat(exception).hasMessageThat().contains("Invalid language case")
+ assertThat(locale).isEqualTo(Locale.ROOT)
}
/* Tests for written content strings. */
@@ -584,7 +636,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -600,7 +652,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -616,7 +668,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_US
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale. Note that BR is matched since the IETF
// language tag includes the region.
@@ -633,7 +685,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -652,7 +704,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -672,7 +724,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -690,7 +742,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -707,7 +759,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -724,7 +776,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language's region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -741,7 +793,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_ZZ
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the supplied region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -758,7 +810,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -775,7 +827,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language is invalid.
assertThat(locale.language).isEqualTo("pt")
@@ -793,7 +845,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language isn't compatible with the current SDK.
assertThat(locale.language).isEqualTo("pt")
@@ -813,7 +865,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -833,7 +885,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -850,7 +902,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is the exact locale being requested.
assertThat(locale.language).isEqualTo("qq")
@@ -870,7 +922,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the IETF language since Android IDs are picked first when
// creating a forced locale.
@@ -891,7 +943,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the macaronic language since Android IDs are picked first when
// creating a forced locale.
@@ -909,7 +961,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The IETF language ID is used for the forced locale (note that fallback languages are ignored
// when computing the forced locale).
@@ -927,7 +979,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Hinglish macaronic language ID is used for the forced locale (note that fallback
// languages are ignored when computing the forced locale).
@@ -946,7 +998,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid ID")
@@ -964,7 +1016,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(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).
@@ -981,7 +1033,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(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).
@@ -999,7 +1051,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid language case")
@@ -1016,7 +1068,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1032,7 +1084,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1048,7 +1100,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_US
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale. Note that BR is matched since the IETF
// language tag includes the region.
@@ -1065,7 +1117,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The context should be matched to a valid locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1084,7 +1136,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -1104,7 +1156,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Android is preferred when both are present. Note no region is provided since the Android
// language is missing a region definition.
@@ -1122,7 +1174,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1139,7 +1191,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1156,7 +1208,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language's region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1173,7 +1225,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_ZZ
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the supplied region doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1190,7 +1242,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language doesn't match a real locale.
assertThat(locale.language).isEqualTo("pt")
@@ -1207,7 +1259,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language is invalid.
assertThat(locale.language).isEqualTo("pt")
@@ -1225,7 +1277,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_BRAZIL
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked because the primary language isn't compatible with the current SDK.
assertThat(locale.language).isEqualTo("pt")
@@ -1245,7 +1297,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -1265,7 +1317,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// pt-BR should be picked since Android IDs take precedence among multiple fallback options.
assertThat(locale.language).isEqualTo("pt")
@@ -1282,7 +1334,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' is the exact locale being requested.
assertThat(locale.language).isEqualTo("qq")
@@ -1302,7 +1354,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the IETF language since Android IDs are picked first when
// creating a forced locale.
@@ -1323,7 +1375,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// 'qq' takes precedence over the macaronic language since Android IDs are picked first when
// creating a forced locale.
@@ -1341,7 +1393,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The IETF language ID is used for the forced locale (note that fallback languages are ignored
// when computing the forced locale).
@@ -1359,7 +1411,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(context)
// The Hinglish macaronic language ID is used for the forced locale (note that fallback
// languages are ignored when computing the forced locale).
@@ -1378,7 +1430,7 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid ID")
@@ -1396,7 +1448,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(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).
@@ -1413,7 +1465,7 @@ class AndroidLocaleFactoryTest {
regionDefinition = REGION_INDIA
)
- val locale = androidLocaleFactory.createAndroidLocale(context)
+ val locale = androidLocaleFactory.createAndroidLocaleBlocking(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).
@@ -1431,12 +1483,22 @@ class AndroidLocaleFactoryTest {
)
val exception = assertThrows() {
- androidLocaleFactory.createAndroidLocale(context)
+ androidLocaleFactory.createAndroidLocaleBlocking(context)
}
assertThat(exception).hasMessageThat().contains("Invalid language case")
}
+ private fun AndroidLocaleFactory.createAndroidLocaleBlocking(
+ context: OppiaLocaleContext
+ ): Locale {
+ val deferred =
+ CoroutineScope(backgroundDispatcher).async { createAndroidLocaleAsync(context).await() }
+ testCoroutineDispatchers.runCurrent()
+ assertThat(deferred.isCompleted).isTrue()
+ return runBlocking { deferred.await() }
+ }
+
private fun createLanguageId(androidLanguageId: AndroidLanguageId): LanguageId {
return LanguageId.newBuilder().apply {
androidResourcesLanguageId = androidLanguageId
@@ -1602,7 +1664,8 @@ class AndroidLocaleFactoryTest {
@Singleton
@Component(
modules = [
- TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class
+ TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class,
+ TestDispatcherModule::class, RobolectricModule::class
]
)
interface TestApplicationComponent {
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
index 42384c8531a..d686a5aaea9 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/locale/AndroidLocaleProfileTest.kt
@@ -12,6 +12,7 @@ import dagger.Provides
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+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
@@ -19,6 +20,11 @@ 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.oppia.android.util.locale.AndroidLocaleProfile.LanguageAndRegionProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.LanguageAndWildcardRegionProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.LanguageOnlyProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.RegionOnlyProfile
+import org.oppia.android.util.locale.AndroidLocaleProfile.RootProfile
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import java.util.Locale
@@ -32,11 +38,10 @@ import javax.inject.Singleton
@LooperMode(LooperMode.Mode.PAUSED)
@Config(manifest = Config.NONE)
class AndroidLocaleProfileTest {
- @Inject
- lateinit var machineLocale: OppiaLocale.MachineLocale
+ @Inject lateinit var androidLocaleProfileFactory: AndroidLocaleProfile.Factory
- private val portugueseLocale by lazy { Locale("pt") }
private val brazilianPortugueseLocale by lazy { Locale("pt", "BR") }
+ private val kenyaOnlyLocale by lazy { Locale(/* language = */ "", "KE") }
@Before
fun setUp() {
@@ -46,27 +51,38 @@ class AndroidLocaleProfileTest {
/* Tests for createFrom */
@Test
- fun testCreateProfile_fromRootLocale_returnsProfileWithoutLanguageAndRegionCode() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ fun testCreateProfile_fromRootLocale_returnsRootProfile() {
+ val profile = androidLocaleProfileFactory.createFrom(Locale.ROOT)
- assertThat(profile.languageCode).isEmpty()
- assertThat(profile.regionCode).isEmpty()
+ assertThat(profile).isEqualTo(RootProfile)
}
@Test
- fun testCreateProfile_fromEnglishLocale_returnsProfileWithLanguageAndWithoutRegion() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ fun testCreateProfile_fromEnglishLocale_returnsLanguageOnlyProfile() {
+ val profile = androidLocaleProfileFactory.createFrom(Locale.ENGLISH)
- assertThat(profile.languageCode).isEqualTo("en")
- assertThat(profile.regionCode).isEmpty()
+ val languageOnlyProfile = profile as? LanguageOnlyProfile
+ assertThat(profile).isInstanceOf(LanguageOnlyProfile::class.java)
+ assertThat(languageOnlyProfile?.languageCode).isEqualTo("en")
}
@Test
fun testCreateProfile_fromBrazilianPortuguese_returnsProfileWithLanguageAndRegion() {
- val profile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ val profile = androidLocaleProfileFactory.createFrom(brazilianPortugueseLocale)
- assertThat(profile.languageCode).isEqualTo("pt")
- assertThat(profile.regionCode).isEqualTo("BR")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("br")
+ }
+
+ @Test
+ fun testCreateProfile_fromKenyaLocale_returnsRegionOnlyProfile() {
+ val profile = androidLocaleProfileFactory.createFrom(kenyaOnlyLocale)
+
+ val regionOnlyProfile = profile as? RegionOnlyProfile
+ assertThat(profile).isInstanceOf(RegionOnlyProfile::class.java)
+ assertThat(regionOnlyProfile?.regionCode).isEqualTo("ke")
}
/* Tests for createFromIetfDefinitions */
@@ -74,7 +90,7 @@ class AndroidLocaleProfileTest {
@Test
fun testCreateProfileFromIetf_defaultLanguageId_nullRegion_returnsNull() {
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(
+ androidLocaleProfileFactory.createFromIetfDefinitions(
languageId = LanguageId.getDefaultInstance(), regionDefinition = null
)
@@ -89,7 +105,8 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithoutIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithoutIetf, REGION_INDIA)
// The language ID needs to have an IETF BCP 47 ID defined.
assertThat(profile).isNull()
@@ -103,7 +120,8 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
// The language ID needs to have an IETF BCP 47 ID defined.
assertThat(profile).isNull()
@@ -117,7 +135,8 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
// The language ID needs to have a well-formed IETF BCP 47 ID defined.
assertThat(profile).isNull()
@@ -131,12 +150,15 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.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")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("in")
}
@Test
@@ -147,11 +169,14 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile = AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, REGION_INDIA)
+ val profile =
+ androidLocaleProfileFactory.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")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("br")
}
@Test
@@ -163,7 +188,7 @@ class AndroidLocaleProfileTest {
}.build()
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(
+ androidLocaleProfileFactory.createFromIetfDefinitions(
languageWithIetf, regionDefinition = RegionSupportDefinition.getDefaultInstance()
)
@@ -180,7 +205,7 @@ class AndroidLocaleProfileTest {
}.build()
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(
+ androidLocaleProfileFactory.createFromIetfDefinitions(
languageWithIetf, regionDefinition = RegionSupportDefinition.getDefaultInstance()
)
@@ -197,11 +222,14 @@ class AndroidLocaleProfileTest {
}.build()
val profile =
- AndroidLocaleProfile.createFromIetfDefinitions(languageWithIetf, regionDefinition = null)
+ androidLocaleProfileFactory.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)
+ val languageAndWildcardRegionProfile = profile as? LanguageAndWildcardRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndWildcardRegionProfile::class.java)
+ assertThat(languageAndWildcardRegionProfile?.languageCode).isEqualTo("pt")
}
/* Tests for createFromMacaronicLanguage */
@@ -209,7 +237,9 @@ class AndroidLocaleProfileTest {
@Test
fun testCreateProfileFromMacaronic_defaultLanguageId_returnsNull() {
val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageId = LanguageId.getDefaultInstance())
+ androidLocaleProfileFactory.createFromMacaronicLanguage(
+ languageId = LanguageId.getDefaultInstance()
+ )
assertThat(profile).isNull()
}
@@ -222,8 +252,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithoutMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithoutMacaronic)
// The provided language ID must have a macaronic ID defined.
assertThat(profile).isNull()
@@ -237,8 +266,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The provided language ID must have a macaronic ID defined.
assertThat(profile).isNull()
@@ -252,8 +280,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The provided language ID must have a well-formed macaronic ID defined.
assertThat(profile).isNull()
@@ -267,8 +294,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The provided language ID must have a well-formed macaronic ID defined, that is, it must have
// two language parts defined.
@@ -283,8 +309,7 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.createFromMacaronicLanguage(languageWithMacaronic)
// The macaronic ID has two parts as expected, but the second language ID must be filled in.
assertThat(profile).isNull()
@@ -298,262 +323,593 @@ class AndroidLocaleProfileTest {
}.build()
}.build()
- val profile =
- AndroidLocaleProfile.createFromMacaronicLanguage(languageWithMacaronic)
+ val profile = androidLocaleProfileFactory.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")
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("hi")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("en")
+ }
+
+ /* Tests for createFromAndroidResourcesLanguageId(). */
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_defaultLanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.getDefaultInstance()
+ )
+
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_ietfBcp47LanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ ietfBcp47Id = IetfBcp47LanguageId.newBuilder().apply {
+ ietfLanguageTag = "pt-BR"
+ }.build()
+ }.build()
+ )
+
+ // This method only creates a profile when provided with a valid Android resources language ID.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_macaronicLanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ macaronicId = MacaronicLanguageId.newBuilder().apply {
+ combinedLanguageCode = "hi-en"
+ }.build()
+ }.build()
+ )
+
+ // This method only creates a profile when provided with a valid Android resources language ID.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_defaultAndroidLanguageId_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.getDefaultInstance()
+ }.build()
+ )
+
+ // This method only creates a profile when provided with a valid Android resources language ID.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_androidLanguageId_regionOnly_returnsNull() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.newBuilder().apply {
+ regionCode = "BR"
+ }.build()
+ }.build()
+ )
+
+ // A valid Android language ID must include at least a language code.
+ assertThat(profile).isNull()
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_androidLanguageId_langOnly_returnsLangWildcard() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.newBuilder().apply {
+ languageCode = "pt"
+ }.build()
+ }.build()
+ )
+
+ // If no region is provided, match against all regions.
+ val languageAndWildcardRegionProfile = profile as? LanguageAndWildcardRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndWildcardRegionProfile::class.java)
+ assertThat(languageAndWildcardRegionProfile?.languageCode).isEqualTo("pt")
+ }
+
+ @Test
+ fun testCreateFromAndroidResourcesLanguageId_androidLanguageId_returnsLangAndRegionProfile() {
+ val profile =
+ androidLocaleProfileFactory.createFromAndroidResourcesLanguageId(
+ languageId = LanguageId.newBuilder().apply {
+ androidResourcesLanguageId = AndroidLanguageId.newBuilder().apply {
+ languageCode = "pt"
+ regionCode = "BR"
+ }.build()
+ }.build()
+ )
+
+ // Both the language & region codes should be represented in the profile.
+ val languageAndRegionProfile = profile as? LanguageAndRegionProfile
+ assertThat(profile).isInstanceOf(LanguageAndRegionProfile::class.java)
+ assertThat(languageAndRegionProfile?.languageCode).isEqualTo("pt")
+ assertThat(languageAndRegionProfile?.regionCode).isEqualTo("br")
}
/* Tests for matches() */
@Test
- fun testMatchProfile_rootProfile_withItself_match() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ROOT)
+ fun testMatchProfile_rootProfile_andRootProfile_matches() {
+ val profile1 = RootProfile
+ val profile2 = RootProfile
- val matches = profile.matches(machineLocale, profile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_englishProfile_withItself_match() {
- val profile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ fun testMatchProfile_rootProfile_andPtLanguageOnly_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
- val matches = profile.matches(machineLocale, profile)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_withItself_match() {
- val profile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_rootProfile_andBrRegionOnly_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 = RegionOnlyProfile(regionCode = "br")
- val matches = profile.matches(machineLocale, profile)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_englishProfile_withItselfInDifferentCase_match() {
- val englishProfileLowercase = AndroidLocaleProfile(languageCode = "en", regionCode = "")
- val englishProfileUppercase = AndroidLocaleProfile(languageCode = "EN", regionCode = "")
+ fun testMatchProfile_rootProfile_andPtBrProfile_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = englishProfileLowercase.matches(machineLocale, englishProfileUppercase)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_englishProfile_withItselfInDifferentCase_reversed_match() {
- val englishProfileLowercase = AndroidLocaleProfile(languageCode = "en", regionCode = "")
- val englishProfileUppercase = AndroidLocaleProfile(languageCode = "EN", regionCode = "")
+ fun testMatchProfile_rootProfile_andPtWildcardProfile_doNotMatch() {
+ val profile1 = RootProfile
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "pt")
- val matches = englishProfileUppercase.matches(machineLocale, englishProfileLowercase)
+ val matches = profile1.matches(profile2)
- assertThat(matches).isTrue()
+ assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_withItselfInDifferentCase_match() {
- val brazilianPortugueseProfileLowercase =
- AndroidLocaleProfile(languageCode = "pt", regionCode = "br")
- val brazilianPortugueseProfileUppercase =
- AndroidLocaleProfile(languageCode = "PT", regionCode = "BR")
+ fun testMatchProfile_ptLanguageOnly_andRootProfile_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = RootProfile
- val matches =
- brazilianPortugueseProfileLowercase.matches(
- machineLocale, brazilianPortugueseProfileUppercase
- )
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andPtLanguageOnly_matches() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
- fun testMatchProfile_brazilianPortuguese_withItselfInDifferentCase_reversed_match() {
- val brazilianPortugueseProfileLowercase =
- AndroidLocaleProfile(languageCode = "pt", regionCode = "br")
- val brazilianPortugueseProfileUppercase =
- AndroidLocaleProfile(languageCode = "PT", regionCode = "BR")
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andSwLanguageOnly_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "sw")
- val matches =
- brazilianPortugueseProfileUppercase.matches(
- machineLocale, brazilianPortugueseProfileLowercase
- )
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andBrRegionOnly_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andPtBrProfile_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptLanguageOnly_andSwWildcardProfile_doNotMatch() {
+ val profile1 = LanguageOnlyProfile(languageCode = "pt")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "sw")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brRegionOnly_andRootProfile_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = RootProfile
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brRegionOnly_andPtLanguageOnly_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_brRegionOnly_andBrRegionOnly_matches() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_rootProfile_english_doNotMatch() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
+ fun testMatchProfile_brRegionOnly_andKeRegionOnly_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = RegionOnlyProfile(regionCode = "ke")
- val matches = rootProfile.matches(machineLocale, englishProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_rootProfile_brazilianPortuguese_doNotMatch() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_brRegionOnly_andPtBrProfile_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = rootProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_english_brazilianPortuguese_doNotMatch() {
- val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_brRegionOnly_andPtWildcardProfile_doNotMatch() {
+ val profile1 = RegionOnlyProfile(regionCode = "br")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "pt")
- val matches = englishProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_rootProfile_englishWithWildcard_doNotMatch() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val englishWithWildcardProfile = createProfileWithWildcard(languageCode = "en")
+ fun testMatchProfile_ptBrProfile_andRootProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = RootProfile
- val matches = rootProfile.matches(machineLocale, englishWithWildcardProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_rootProfile_rootProfileWithWildcard_match() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+ fun testMatchProfile_ptBrProfile_andPtLanguageOnly_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
- val matches = rootProfile.matches(machineLocale, rootProfileWithWildcardProfile)
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andBrRegionOnly_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andPtBrProfile_matches() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_rootProfileWithWildcard_rootProfile_match() {
- val rootProfile = AndroidLocaleProfile.createFrom(Locale.ROOT)
- val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+ fun testMatchProfile_ptBrProfile_andSwBrProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = rootProfileWithWildcardProfile.matches(machineLocale, rootProfile)
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andPtKeProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "ke", regionCodeUpperCase = "KE")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andSwKeProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "ke", regionCodeUpperCase = "KE")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptBrProfile_andSwWildcardProfile_doNotMatch() {
+ val profile1 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "sw")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andRootProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = RootProfile
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andPtLanguageOnly_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_englishProfile_rootProfileWithWildcard_doNotMatch() {
- val englishProfile = AndroidLocaleProfile.createFrom(Locale.ENGLISH)
- val rootProfileWithWildcardProfile = createProfileWithWildcard(languageCode = "")
+ fun testMatchProfile_ptWildcardProfile_andSwLanguageOnly_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageOnlyProfile(languageCode = "sw")
- val matches = englishProfile.matches(machineLocale, rootProfileWithWildcardProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_englishWithWildcard_brazilianPortuguese_doNotMatch() {
- val englishWithWildcardProfile = createProfileWithWildcard(languageCode = "en")
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andBrRegionOnly_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = RegionOnlyProfile(regionCode = "br")
- val matches = englishWithWildcardProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_portuguese_doNotMatch() {
- val portugueseProfile = AndroidLocaleProfile.createFrom(portugueseLocale)
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andPtBrProfile_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andSwBrProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "br", regionCodeUpperCase = "BR")
- val matches = portugueseProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isFalse()
}
@Test
- fun testMatchProfile_brazilianPortuguese_portugueseWithWildcard_match() {
- val portugueseWithWildcardProfile = createProfileWithWildcard(languageCode = "pt")
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andPtKeProfile_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "ke", regionCodeUpperCase = "KE")
- val matches = brazilianPortugueseProfile.matches(machineLocale, portugueseWithWildcardProfile)
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
@Test
- fun testMatchProfile_portugueseWithWildcard_brazilianPortuguese_match() {
- val portugueseWithWildcardProfile = createProfileWithWildcard(languageCode = "pt")
- val brazilianPortugueseProfile = AndroidLocaleProfile.createFrom(brazilianPortugueseLocale)
+ fun testMatchProfile_ptWildcardProfile_andSwKeProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 =
+ LanguageAndRegionProfile(languageCode = "sw", regionCode = "ke", regionCodeUpperCase = "KE")
- val matches = portugueseWithWildcardProfile.matches(machineLocale, brazilianPortugueseProfile)
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andPtWildcardProfile_matches() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+
+ val matches = profile1.matches(profile2)
assertThat(matches).isTrue()
}
+ @Test
+ fun testMatchProfile_ptWildcardProfile_andSwWildcardProfile_doNotMatch() {
+ val profile1 = LanguageAndWildcardRegionProfile(languageCode = "pt")
+ val profile2 = LanguageAndWildcardRegionProfile(languageCode = "sw")
+
+ val matches = profile1.matches(profile2)
+
+ assertThat(matches).isFalse()
+ }
+
/* Tests for computeIetfLanguageTag */
@Test
- fun testComputeIetfLanguageTag_noLanguageCode_noRegionCode_returnsEmptyString() {
- val emptyProfile = AndroidLocaleProfile(languageCode = "", regionCode = "")
+ fun testIetfLanguageTag_rootProfile_isEmptyString() {
+ val profile = RootProfile
- val ietfLanguageTag = emptyProfile.computeIetfLanguageTag()
+ val ietfLanguageTag = profile.ietfLanguageTag
assertThat(ietfLanguageTag).isEmpty()
}
@Test
- fun testComputeIetfLanguageTag_languageCode_noRegionCode_returnsLanguageCode() {
- val portugueseProfile = AndroidLocaleProfile(languageCode = "pt", regionCode = "")
+ fun testIetfLanguageTag_languageOnlyProfile_isLanguageCode() {
+ val profile = LanguageOnlyProfile(languageCode = "pt")
- val ietfLanguageTag = portugueseProfile.computeIetfLanguageTag()
+ val ietfLanguageTag = profile.ietfLanguageTag
assertThat(ietfLanguageTag).isEqualTo("pt")
}
@Test
- fun testComputeIetfLanguageTag_languageCode_wildcardRegionCode_returnsLanguageCode() {
- val portugueseAndRegionProfile = createProfileWithWildcard(languageCode = "pt")
+ fun testIetfLanguageTag_regionOnlyProfile_isRegionCode() {
+ val profile = RegionOnlyProfile(regionCode = "br")
- val ietfLanguageTag = portugueseAndRegionProfile.computeIetfLanguageTag()
+ val ietfLanguageTag = profile.ietfLanguageTag
+
+ assertThat(ietfLanguageTag).isEqualTo("br")
+ }
+
+ @Test
+ fun testIetfLanguageTag_languageWithRegionProfile_isIetfBcp47CombinedLanguageTag() {
+ val profile =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val ietfLanguageTag = profile.ietfLanguageTag
+
+ assertThat(ietfLanguageTag).isEqualTo("pt-BR")
+ }
+
+ @Test
+ fun testIetfLanguageTag_languageWithWildcardProfile_isLanguageCode() {
+ val profile = LanguageAndWildcardRegionProfile(languageCode = "pt")
+
+ val ietfLanguageTag = profile.ietfLanguageTag
// The wildcard shouldn't be part of the IETF BCP 47 tag since that standard doesn't define such
// a concept.
assertThat(ietfLanguageTag).isEqualTo("pt")
}
+ /* Tests for computeAndroidLocale() */
+
@Test
- fun testComputeIetfLanguageTag_noLanguageCode_regionCode_returnsRegionCode() {
- val brazilianProfile = AndroidLocaleProfile(languageCode = "", regionCode = "BR")
+ fun testComputeAndroidLocale_rootProfile_returnsRootLocale() {
+ val profile = RootProfile
- val ietfLanguageTag = brazilianProfile.computeIetfLanguageTag()
+ val locale = profile.computeAndroidLocale()
- assertThat(ietfLanguageTag).isEqualTo("BR")
+ assertThat(locale).isEqualTo(Locale.ROOT)
}
@Test
- fun testComputeIetfLanguageTag_noLanguageCode_wildcardRegionCode_returnsEmptyString() {
- val matchNothingProfile = createProfileWithWildcard(languageCode = "")
+ fun testComputeAndroidLocale_languageOnlyProfile_returnsLocaleWithLanguageAndEmptyCountry() {
+ val profile = LanguageOnlyProfile(languageCode = "pt")
- val ietfLanguageTag = matchNothingProfile.computeIetfLanguageTag()
+ val locale = profile.computeAndroidLocale()
- assertThat(ietfLanguageTag).isEmpty()
+ assertThat(locale.language).ignoringCase().isEqualTo("pt")
+ assertThat(locale.country).isEmpty()
}
@Test
- fun testComputeIetfLanguageTag_languageCode_regionCode_returnsIetfBcp47CombinedLanguageTag() {
- val brazilianPortugueseProfile = AndroidLocaleProfile(languageCode = "pt", regionCode = "BR")
+ fun testComputeAndroidLocale_regionOnlyProfile_returnsLocaleWithCountryAndEmptyLanguage() {
+ val profile = RegionOnlyProfile(regionCode = "br")
- val ietfLanguageTag = brazilianPortugueseProfile.computeIetfLanguageTag()
+ val locale = profile.computeAndroidLocale()
- assertThat(ietfLanguageTag).isEqualTo("pt-BR")
+ assertThat(locale.country).ignoringCase().isEqualTo("br")
+ assertThat(locale.language).isEmpty()
+ }
+
+ @Test
+ fun testComputeAndroidLocale_languageWithWildcardProfile_returnsLocaleWithLangAndEmptyCountry() {
+ val profile = LanguageAndWildcardRegionProfile(languageCode = "pt")
+
+ val locale = profile.computeAndroidLocale()
+
+ assertThat(locale.language).ignoringCase().isEqualTo("pt")
+ assertThat(locale.country).isEmpty()
}
- private fun createProfileWithWildcard(languageCode: String): AndroidLocaleProfile =
- AndroidLocaleProfile(languageCode, regionCode = AndroidLocaleProfile.REGION_WILDCARD)
+ @Test
+ fun testComputeAndroidLocale_languageAndRegionProfile_returnsLocaleWithLanguageAndCountry() {
+ val profile =
+ LanguageAndRegionProfile(languageCode = "pt", regionCode = "br", regionCodeUpperCase = "BR")
+
+ val locale = profile.computeAndroidLocale()
+
+ assertThat(locale.language).ignoringCase().isEqualTo("pt")
+ assertThat(locale.country).ignoringCase().isEqualTo("br")
+ }
private fun setUpTestApplicationComponent() {
DaggerAndroidLocaleProfileTest_TestApplicationComponent.builder()
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
index 850e0d05370..d1bc33a3988 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel
+++ b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel
@@ -14,6 +14,8 @@ oppia_android_test(
deps = [
":dagger",
"//testing",
+ "//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",
@@ -72,6 +74,8 @@ oppia_android_test(
":dagger",
"//model/src/main/proto:languages_java_proto_lite",
"//testing",
+ "//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",
diff --git a/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt b/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt
index e300a47d6fa..dfc1ba84b79 100644
--- a/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/locale/DisplayLocaleImplTest.kt
@@ -21,6 +21,8 @@ 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.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.locale.testing.LocaleTestModule
import org.oppia.android.util.locale.testing.TestOppiaBidiFormatter
@@ -56,13 +58,6 @@ class DisplayLocaleImplTest {
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)
@@ -72,7 +67,7 @@ class DisplayLocaleImplTest {
@Test
fun testToString_returnsNonDefaultString() {
- val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
val str = impl.toString()
@@ -83,14 +78,14 @@ class DisplayLocaleImplTest {
@Test
fun testEquals_withNullValue_returnsFalse() {
- val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
assertThat(impl).isNotEqualTo(null)
}
@Test
fun testEquals_withSameObject_returnsTrue() {
- val impl = createDisplayLocaleImpl(OppiaLocaleContext.getDefaultInstance())
+ val impl = createDisplayLocaleImpl(EGYPT_ARABIC_CONTEXT)
assertThat(impl).isEqualTo(impl)
}
@@ -615,8 +610,10 @@ class DisplayLocaleImplTest {
}
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun setUpTestApplicationComponent() {
DaggerDisplayLocaleImplTest_TestApplicationComponent.builder()
@@ -639,7 +636,8 @@ class DisplayLocaleImplTest {
@Singleton
@Component(
modules = [
- TestModule::class, LocaleTestModule::class, FakeOppiaClockModule::class
+ TestModule::class, LocaleTestModule::class, FakeOppiaClockModule::class,
+ TestDispatcherModule::class, RobolectricModule::class
]
)
interface TestApplicationComponent {
diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt
index 5cba77e357d..2042ef72ec8 100644
--- a/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt
@@ -311,8 +311,10 @@ class CustomHtmlContentHandlerTest {
)
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private class FakeTagHandler : CustomHtmlContentHandler.CustomTagHandler {
var handleTagCalled = false
diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
index 46e4ca83387..1341bc68776 100644
--- a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt
@@ -137,8 +137,10 @@ class LiTagHandlerTest {
.hasLength(4)
}
- private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl =
- DisplayLocaleImpl(context, machineLocale, androidLocaleFactory, formatterFactory)
+ private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl {
+ val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context)
+ return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory)
+ }
private fun Spannable.getSpansFromWholeString(spanClass: KClass): Array =
getSpans(/* start= */ 0, /* end= */ length, spanClass.javaObjectType)
diff --git a/wiki/Updating-Maven-Dependencies.md b/wiki/Updating-Maven-Dependencies.md
index 6e0653f844b..353de7a5d7c 100644
--- a/wiki/Updating-Maven-Dependencies.md
+++ b/wiki/Updating-Maven-Dependencies.md
@@ -18,7 +18,7 @@ cd ~/opensource/oppia-android
The above command ensures that the terminal points to the root directory `oppia-android` repository. Note that if you have configured a different path to the `oppia-android` repository then you should modify the above command accordingly ( `cd ~/` ).
#### Running `GenerateMavenDependenciesList.kt` script
-After the terminal points to the Oppia-android repository, run the bazel run command to execute the Kotlin script.
+After the terminal points to the Oppia-android repository, run the bazel run command to execute the Kotlin script.
```
bazel run //scripts:generate_maven_dependencies_list -- $(pwd) third_party/maven_install.json scripts/assets/maven_dependencies.textproto scripts/assets/maven_dependencies.pb
```
@@ -26,19 +26,19 @@ bazel run //scripts:generate_maven_dependencies_list -- $(pwd) third_party/maven
## Handling Exception: `Too few arguments passed`
If after running the script the exception message says: **Too few arguments passed**, then please ensure that you copied the command correctly from [here](https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies#running-generatemavendependencieslistkt-script).
The script accepts 4 parameters to be passed to run successfully:
-1. **_path_to_directory_root_**: directory path to the root of the Oppia Android repository, e.g - `home//opensource/oppia-android`
-2. **_path_to_maven_install_json_**: relative path to the maven_install.json file, e.g - `third_party/maven_install.json`
-3. **_path_to_maven_dependencies_textproto_**: relative path to the maven_dependencies.textproto, e.g - `scripts/assets/maven_dependencies.textproto`
-4. **_path_to_maven_dependencies_pb_**: relative path to the maven_dependencies.pb file, e.g - `scripts/assets/maven_dependencies.pb`
+1. **_path_to_directory_root_**: directory path to the root of the Oppia Android repository, e.g. - `home//opensource/oppia-android`
+2. **_path_to_maven_install_json_**: relative path to the maven_install.json file, e.g. - `third_party/maven_install.json`
+3. **_path_to_maven_dependencies_textproto_**: relative path to the maven_dependencies.textproto, e.g. - `scripts/assets/maven_dependencies.textproto`
+4. **_path_to_maven_dependencies_pb_**: relative path to the maven_dependencies.pb file, e.g. - `scripts/assets/maven_dependencies.pb`
## Handling Exception: `Licenses details are not completed`
The script can take about a minute to execute, and if the script fails with the exception: `Licenses details are not completed`, you will need to do some manual work in `maven_dependencies.textproto`.
-The script would call out specific dependencies that need to be updated manually, e.g -
+The script would call out specific dependencies that need to be updated manually, e.g. -
```
-Please verify the license link(s) for the following license(s) manually in
-maven_dependencies.textproto, note that only the first dependency that contains the license
+Please verify the license link(s) for the following license(s) manually in
+maven_dependencies.textproto, note that only the first dependency that contains the license
needs to be updated and also re-run the script to update the license details at all places:
license_name: Android Software Development Kit License
@@ -67,14 +67,14 @@ maven_dependency {
### Categorizing the license link
If the link does point to a valid license then choose the most appropriate category for the link:
-1. scrapable_link: If the license text is plain text and the URL mentioned can be scraped directly from the original_link of the license.
- e.g - https://www.apache.org/licenses/LICENSE-2.0.txt
+1. scrapable_link: If the license text is plain text and the URL mentioned can be scraped directly from the original_link of the license.
+ e.g. - https://www.apache.org/licenses/LICENSE-2.0.txt
2. extracted_copy_link: If the license text is plain text but can not be scraped directly from the original_link of the license.
- e.g - https://opensource.org/license/bsd-3-clause
+ e.g. - https://opensource.org/license/bsd-3-clause
3. direct_link_only: If the license text is not plain text, it's best to display only the link of the license.
- e.g - https://developer.android.com/studio/terms.html
+ e.g. - https://developer.android.com/studio/terms.html
-After identifying the category of the license, modify the license to include one of the above-mentioned 'url'. e.g -
+After identifying the category of the license, modify the license to include one of the above-mentioned 'url'. e.g. -
```
license {
license_name: "The Apache Software License, Version 2.0"
@@ -86,7 +86,7 @@ license {
```
Also, if the license falls in the `extracted_copy_link` category, then go to [Oppia-android-licenses](https://github.com/oppia/oppia-android-licenses) and find if there exists a copy of the license already in the repository. If there exists a copy of the license, perform the following steps to get the link for the license that can be scraped easily.
-1. Click on the appropriate license file.
+1. Click on the appropriate license file.
2. Now click on the raw button positioned in the left of the edit and delete button.
3. Copy the URL from the browser and mention it at the appropriate place.
@@ -95,7 +95,7 @@ If the license does not exist in the Oppia-android-licenses repository, then coo
-After modifying `maven_dependencies.textproto` for all the called out licenses in the console output, re-run the script and see if any other error occurs.
+After modifying `maven_dependencies.textproto` for all the called out licenses in the console output, re-run the script and see if any other error occurs.
## Handling Exception: `License links are invalid or not available for some dependencies`
@@ -122,7 +122,7 @@ maven_dependency {
To fix the error, consider the above example. For the first maven_dependency: "io.fabric.sdk.android:fabric:1.4.7", the original_link is invalid, and hence we need to find a valid link for this dependency. Please coordinate with the Oppia Android team and find a valid link for this dependency. Once you have a valid link for this license then categorize it as mentioned [here](https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies#categorizing-the-license-link).
-For the second maven_dependency: "com.google.guava:failureaccess:1.0.1", you need to find a license by coordinating with the Oppia Android team and then specify it under the artifact_version field of the dependency. e.g -
+For the second maven_dependency: "com.google.guava:failureaccess:1.0.1", you need to find a license by coordinating with the Oppia Android team and then specify it under the artifact_version field of the dependency. e.g. -
```
maven_dependency {