diff --git a/.gitignore b/.gitignore index f9250e37d80..6d3b741a2f7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ config/oppia-dev-workflow-remote-cache-credentials.json bazel-* .bazelproject .aswb +*.pb diff --git a/app/BUILD.bazel b/app/BUILD.bazel index da637339c1a..fba131c801b 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -17,7 +17,10 @@ load("@tools_android//tools/googleservices:defs.bzl", "google_services_xml") load("//app:app_test.bzl", "app_test") load("//app:test_with_resources.bzl", "test_with_resources") -package(default_visibility = ["//utility:__subpackages__"]) +package(default_visibility = [ + "//domain:__subpackages__", + "//utility:__subpackages__", +]) exports_files(["src/main/AndroidManifest.xml"]) @@ -792,6 +795,7 @@ TEST_DEPS = [ "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/espresso:edit_text_input_action", "//testing/src/main/java/org/oppia/android/testing/espresso:generic_view_matchers", "//testing/src/main/java/org/oppia/android/testing/espresso:image_view_matcher", diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt index 6bf2f3492a5..af9f96121d1 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragment.kt @@ -10,6 +10,7 @@ import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableDialogFragment import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.State +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.util.extensions.getProto import org.oppia.android.util.extensions.getStringFromBundle import org.oppia.android.util.extensions.putProto @@ -44,25 +45,32 @@ class HintsAndSolutionDialogFragment : internal const val ID_ARGUMENT_KEY = "HintsAndSolutionDialogFragment.id" internal const val STATE_KEY = "HintsAndSolutionDialogFragment.state" internal const val HELP_INDEX_KEY = "HintsAndSolutionDialogFragment.help_index" + internal const val WRITTEN_TRANSLATION_CONTEXT_KEY = + "HintsAndSolutionDialogFragment.written_translation_context" /** * Creates a new instance of a DialogFragment to display hints and solution * - * @param id Used in ExplorationController/QuestionAssessmentProgressController to get current state data. + * @param id Used in ExplorationController/QuestionAssessmentProgressController to get current + * state data. * @param state the [State] being viewed by the learner * @param helpIndex the [HelpIndex] corresponding to the current hints/solution configuration + * @param writtenTranslationContext the [WrittenTranslationContext] needed to translate the + * hints/solution * @return [HintsAndSolutionDialogFragment]: DialogFragment */ fun newInstance( id: String, state: State, - helpIndex: HelpIndex + helpIndex: HelpIndex, + writtenTranslationContext: WrittenTranslationContext ): HintsAndSolutionDialogFragment { return HintsAndSolutionDialogFragment().apply { arguments = Bundle().apply { putString(ID_ARGUMENT_KEY, id) putProto(STATE_KEY, state) putProto(HELP_INDEX_KEY, helpIndex) + putProto(WRITTEN_TRANSLATION_CONTEXT_KEY, writtenTranslationContext) } } } @@ -107,12 +115,15 @@ class HintsAndSolutionDialogFragment : val state = args.getProto(STATE_KEY, State.getDefaultInstance()) val helpIndex = args.getProto(HELP_INDEX_KEY, HelpIndex.getDefaultInstance()) + val writtenTranslationContext = + args.getProto(WRITTEN_TRANSLATION_CONTEXT_KEY, WrittenTranslationContext.getDefaultInstance()) return hintsAndSolutionDialogFragmentPresenter.handleCreateView( inflater, container, state, helpIndex, + writtenTranslationContext, id, currentExpandedHintListIndex, this as ExpandedHintListIndexListener, diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt index a7038603c33..50a77998d9e 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt @@ -12,6 +12,7 @@ import org.oppia.android.app.model.HelpIndex.IndexTypeCase.LATEST_REVEALED_HINT_ import org.oppia.android.app.model.HelpIndex.IndexTypeCase.NEXT_AVAILABLE_HINT_INDEX import org.oppia.android.app.model.HelpIndex.IndexTypeCase.SHOW_SOLUTION import org.oppia.android.app.model.State +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ViewModelProvider @@ -47,6 +48,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( private lateinit var binding: HintsAndSolutionFragmentBinding private lateinit var state: State private lateinit var helpIndex: HelpIndex + private lateinit var writtenTranslationContext: WrittenTranslationContext private lateinit var itemList: List private lateinit var bindingAdapter: BindableAdapter @@ -63,6 +65,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( container: ViewGroup?, state: State, helpIndex: HelpIndex, + writtenTranslationContext: WrittenTranslationContext, id: String?, currentExpandedHintListIndex: Int?, expandedHintListIndexListener: ExpandedHintListIndexListener, @@ -93,6 +96,7 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( this.state = state this.helpIndex = helpIndex + this.writtenTranslationContext = writtenTranslationContext // The newAvailableHintIndex received here is coming from state player but in this // implementation hints/solutions are shown on every even index and on every odd index we show a // divider. The relative index therefore needs to be doubled to account for the divider. @@ -137,7 +141,9 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( private fun loadHintsAndSolution(state: State) { // Check if hints are available for this state. if (state.interaction.hintList.isNotEmpty()) { - viewModel.initialize(helpIndex, state.interaction.hintList, state.interaction.solution) + viewModel.initialize( + helpIndex, state.interaction.hintList, state.interaction.solution, writtenTranslationContext + ) itemList = viewModel.processHintList() diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt index 36b5af0b090..20de8ea0b51 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsViewModel.kt @@ -7,9 +7,11 @@ import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.Hint import org.oppia.android.app.model.Solution +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.hintsandsolution.isHintRevealed import org.oppia.android.domain.hintsandsolution.isSolutionRevealed +import org.oppia.android.domain.translation.TranslationController import javax.inject.Inject /** @@ -24,7 +26,8 @@ private const val DEFAULT_HINT_AND_SOLUTION_SUMMARY = "" /** [ViewModel] for Hints in [HintsAndSolutionDialogFragment]. */ @FragmentScope class HintsViewModel @Inject constructor( - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : HintsAndSolutionItemViewModel() { val newAvailableHintIndex = ObservableField(-1) @@ -39,13 +42,20 @@ class HintsViewModel @Inject constructor( private lateinit var hintList: List private lateinit var solution: Solution private lateinit var helpIndex: HelpIndex + private lateinit var writtenTranslationContext: WrittenTranslationContext val itemList: MutableList = ArrayList() /** Initializes the view model to display hints and a solution. */ - fun initialize(helpIndex: HelpIndex, hintList: List, solution: Solution) { + fun initialize( + helpIndex: HelpIndex, + hintList: List, + solution: Solution, + writtenTranslationContext: WrittenTranslationContext + ) { this.helpIndex = helpIndex this.hintList = hintList this.solution = solution + this.writtenTranslationContext = writtenTranslationContext } fun processHintList(): List { @@ -93,9 +103,11 @@ class HintsViewModel @Inject constructor( } private fun addHintToList(hintIndex: Int, hint: Hint) { - val hintsViewModel = HintsViewModel(resourceHandler) + val hintsViewModel = HintsViewModel(resourceHandler, translationController) hintsViewModel.title.set(hint.hintContent.contentId) - hintsViewModel.hintsAndSolutionSummary.set(hint.hintContent.html) + val hintContentHtml = + translationController.extractString(hint.hintContent, writtenTranslationContext) + hintsViewModel.hintsAndSolutionSummary.set(hintContentHtml) hintsViewModel.isHintRevealed.set(helpIndex.isHintRevealed(hintIndex, hintList)) itemList.add(hintsViewModel) addDividerItem() @@ -109,7 +121,9 @@ class HintsViewModel @Inject constructor( solutionViewModel.denominator.set(solution.correctAnswer.denominator) solutionViewModel.wholeNumber.set(solution.correctAnswer.wholeNumber) solutionViewModel.isNegative.set(solution.correctAnswer.isNegative) - solutionViewModel.solutionSummary.set(solution.explanation.html) + val explanationHtml = + translationController.extractString(solution.explanation, writtenTranslationContext) + solutionViewModel.solutionSummary.set(explanationHtml) solutionViewModel.isSolutionRevealed.set(helpIndex.isSolutionRevealed()) itemList.add(solutionViewModel) addDividerItem() diff --git a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt index 0968ec1f88b..996d08cb7ce 100755 --- a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt @@ -241,7 +241,9 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( if (promotedStory.chapterPlayState == ChapterPlayState.IN_PROGRESS_SAVED) { val explorationCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - ProfileId.getDefaultInstance(), + ProfileId.newBuilder().apply { + internalId = internalProfileId + }.build(), promotedStory.explorationId ).toLiveData() diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt index ccbde8f7948..72d9a729f69 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt @@ -15,6 +15,7 @@ import org.oppia.android.app.hintsandsolution.RevealSolutionInterface import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.model.State +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.audio.AudioButtonListener import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListener import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener @@ -45,6 +46,7 @@ class ExplorationActivity : private lateinit var storyId: String private lateinit var explorationId: String private lateinit var state: State + private lateinit var writtenTranslationContext: WrittenTranslationContext private var backflowScreen: Int? = null private var isCheckpointingEnabled: Boolean = false @@ -165,7 +167,8 @@ class ExplorationActivity : val hintsAndSolutionDialogFragment = HintsAndSolutionDialogFragment.newInstance( explorationId, state, - helpIndex + helpIndex, + writtenTranslationContext ) hintsAndSolutionDialogFragment.showNow(supportFragmentManager, TAG_HINTS_AND_SOLUTION_DIALOG) } @@ -179,8 +182,12 @@ class ExplorationActivity : explorationActivityPresenter.loadExplorationFragment(readingTextSize) } - override fun onExplorationStateLoaded(state: State) { + override fun onExplorationStateLoaded( + state: State, + writtenTranslationContext: WrittenTranslationContext + ) { this.state = state + this.writtenTranslationContext = writtenTranslationContext } override fun dismissConceptCard() = explorationActivityPresenter.dismissConceptCard() diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt index 7b71557a0bd..a8460e014ff 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt @@ -57,7 +57,7 @@ class HintsAndSolutionExplorationManagerFragmentPresenter @Inject constructor( // Check if hints are available for this state. if (ephemeralState.state.interaction.hintList.size != 0) { (activity as HintsAndSolutionExplorationManagerListener).onExplorationStateLoaded( - ephemeralState.state + ephemeralState.state, ephemeralState.writtenTranslationContext ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerListener.kt b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerListener.kt index 9b671eb020e..c728304b65e 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerListener.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerListener.kt @@ -1,8 +1,9 @@ package org.oppia.android.app.player.exploration import org.oppia.android.app.model.State +import org.oppia.android.app.model.WrittenTranslationContext /** Listener for fetching current exploration state data. */ interface HintsAndSolutionExplorationManagerListener { - fun onExplorationStateLoaded(state: State) + fun onExplorationStateLoaded(state: State, writtenTranslationContext: WrittenTranslationContext) } diff --git a/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt b/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt index 4bd48436b97..0a0f06df55c 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt @@ -7,6 +7,7 @@ import androidx.databinding.BindingAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.itemviewmodel.SelectionInteractionContentViewModel import org.oppia.android.app.player.state.itemviewmodel.SelectionItemInputType import org.oppia.android.app.recyclerview.BindableAdapter @@ -45,6 +46,7 @@ class SelectionInteractionView @JvmOverloads constructor( lateinit var bindingInterface: ViewBindingShim private lateinit var entityId: String + private lateinit var writtenTranslationContext: WrittenTranslationContext override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -69,6 +71,15 @@ class SelectionInteractionView @JvmOverloads constructor( this.entityId = entityId } + /** + * Sets the [WrittenTranslationContext] used to translate strings in this view. + * + * This must be called during view initialization. + */ + fun setWrittenTranslationContext(writtenTranslationContext: WrittenTranslationContext) { + this.writtenTranslationContext = writtenTranslationContext + } + private fun createAdapter(): BindableAdapter { return when (selectionItemInputType) { SelectionItemInputType.CHECKBOXES -> @@ -89,7 +100,8 @@ class SelectionInteractionView @JvmOverloads constructor( htmlParserFactory, resourceBucketName, entityType, - entityId + entityId, + writtenTranslationContext ) } ) @@ -112,7 +124,8 @@ class SelectionInteractionView @JvmOverloads constructor( htmlParserFactory, resourceBucketName, entityType, - entityId + entityId, + writtenTranslationContext ) } ) @@ -127,3 +140,10 @@ fun setEntityId( selectionInteractionView: SelectionInteractionView, entityId: String ) = selectionInteractionView.setEntityId(entityId) + +/** Sets the translation context for a specific [SelectionInteractionView] via data-binding. */ +@BindingAdapter("writtenTranslationContext") +fun setWrittenTranslationContext( + selectionInteractionView: SelectionInteractionView, + writtenTranslationContext: WrittenTranslationContext +) = selectionInteractionView.setWrittenTranslationContext(writtenTranslationContext) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index 30a57cce8c5..61bd4999f07 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -112,7 +112,7 @@ class StateFragmentPresenter @Inject constructor( /* attachToRoot= */ false ) recyclerViewAssembler = createRecyclerViewAssembler( - assemblerBuilderFactory.create(resourceBucketName, entityType), + assemblerBuilderFactory.create(resourceBucketName, entityType, profileId), binding.congratulationsTextView, binding.congratulationsTextConfettiView, binding.fullScreenConfettiView @@ -275,7 +275,7 @@ class StateFragmentPresenter @Inject constructor( private fun subscribeToCurrentState() { ephemeralStateLiveData.observe( fragment, - Observer { result -> + { result -> processEphemeralStateResult(result) } ) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 58831b7aa0e..99e21d420d7 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -23,9 +23,11 @@ import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.EphemeralState.StateTypeCase import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.Interaction +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.StringList import org.oppia.android.app.model.SubtitledHtml import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.audio.AudioUiManager import org.oppia.android.app.player.state.StatePlayerRecyclerViewAssembler.Builder.Factory import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver @@ -84,6 +86,7 @@ import org.oppia.android.databinding.SubmittedAnswerItemBinding import org.oppia.android.databinding.SubmittedAnswerListItemBinding import org.oppia.android.databinding.SubmittedHtmlAnswerItemBinding import org.oppia.android.databinding.TextInputInteractionItemBinding +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.parser.html.HtmlParser import org.oppia.android.util.threading.BackgroundDispatcher import javax.inject.Inject @@ -120,6 +123,7 @@ class StatePlayerRecyclerViewAssembler private constructor( val rhsAdapter: BindableAdapter, private val playerFeatureSet: PlayerFeatureSet, private val fragment: Fragment, + private val profileId: ProfileId, private val context: Context, private val congratulationsTextView: TextView?, private val congratulationsTextConfettiView: KonfettiView?, @@ -135,7 +139,8 @@ class StatePlayerRecyclerViewAssembler private constructor( String, @JvmSuppressWildcards InteractionViewModelFactory>, backgroundCoroutineDispatcher: CoroutineDispatcher, private val hasConversationView: Boolean, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : HtmlParser.CustomOppiaTagActionListener { /** * A list of view models corresponding to past view models that are hidden by default. These are @@ -176,7 +181,7 @@ class StatePlayerRecyclerViewAssembler private constructor( override fun onConceptCardLinkClicked(view: View, skillId: String) { ConceptCardFragment - .newInstance(skillId) + .newInstance(skillId, profileId) .showNow(fragment.childFragmentManager, CONCEPT_CARD_DIALOG_FRAGMENT_TAG) } @@ -213,7 +218,8 @@ class StatePlayerRecyclerViewAssembler private constructor( extraInteractionPendingItemList, ephemeralState.pendingState.wrongAnswerList, /* isCorrectAnswer= */ false, - gcsEntityId + gcsEntityId, + ephemeralState.writtenTranslationContext ) if (playerFeatureSet.interactionSupport) { val interactionItemList = @@ -222,7 +228,8 @@ class StatePlayerRecyclerViewAssembler private constructor( interactionItemList, interaction, hasPreviousState, - gcsEntityId + gcsEntityId, + ephemeralState.writtenTranslationContext ) } } else if (ephemeralState.stateTypeCase == StateTypeCase.COMPLETED_STATE) { @@ -242,7 +249,8 @@ class StatePlayerRecyclerViewAssembler private constructor( extraInteractionPendingItemList, ephemeralState.completedState.answerList, /* isCorrectAnswer= */ true, - gcsEntityId + gcsEntityId, + ephemeralState.writtenTranslationContext ) } @@ -295,7 +303,8 @@ class StatePlayerRecyclerViewAssembler private constructor( pendingItemList: MutableList, interaction: Interaction, hasPreviousButton: Boolean, - gcsEntityId: String + gcsEntityId: String, + writtenTranslationContext: WrittenTranslationContext ) { val interactionViewModelFactory = interactionViewModelFactoryMap.getValue(interaction.id) pendingItemList += interactionViewModelFactory( @@ -305,7 +314,8 @@ class StatePlayerRecyclerViewAssembler private constructor( fragment as InteractionAnswerReceiver, fragment as InteractionAnswerErrorOrAvailabilityCheckReceiver, hasPreviousButton, - isSplitView.get()!! + isSplitView.get()!!, + writtenTranslationContext ) } @@ -314,9 +324,12 @@ class StatePlayerRecyclerViewAssembler private constructor( ephemeralState: EphemeralState, gcsEntityId: String ) { - val contentSubtitledHtml: SubtitledHtml = ephemeralState.state.content + val contentSubtitledHtml = + translationController.extractString( + ephemeralState.state.content, ephemeralState.writtenTranslationContext + ) pendingItemList += ContentViewModel( - contentSubtitledHtml.html, + contentSubtitledHtml, gcsEntityId, hasConversationView, isSplitView.get()!!, @@ -329,7 +342,8 @@ class StatePlayerRecyclerViewAssembler private constructor( rightPendingItemList: MutableList, answersAndResponses: List, isCorrectAnswer: Boolean, - gcsEntityId: String + gcsEntityId: String, + writtenTranslationContext: WrittenTranslationContext ) { if (answersAndResponses.size > 1) { if (playerFeatureSet.wrongAnswerCollapsing) { @@ -364,7 +378,8 @@ class StatePlayerRecyclerViewAssembler private constructor( if (playerFeatureSet.feedbackSupport) { createFeedbackItem( answerAndResponse.feedback, - gcsEntityId + gcsEntityId, + writtenTranslationContext )?.let { viewModel -> if (showPreviousAnswers) { pendingItemList += viewModel @@ -391,7 +406,7 @@ class StatePlayerRecyclerViewAssembler private constructor( } } if (playerFeatureSet.feedbackSupport) { - createFeedbackItem(answerAndResponse.feedback, gcsEntityId)?.let( + createFeedbackItem(answerAndResponse.feedback, gcsEntityId, writtenTranslationContext)?.let( pendingItemList::add ) } @@ -539,12 +554,14 @@ class StatePlayerRecyclerViewAssembler private constructor( private fun createFeedbackItem( feedback: SubtitledHtml, - gcsEntityId: String + gcsEntityId: String, + writtenTranslationContext: WrittenTranslationContext ): FeedbackViewModel? { // Only show feedback if there's some to show. - if (feedback.html.isNotEmpty()) { + val feedbackHtml = translationController.extractString(feedback, writtenTranslationContext) + if (feedbackHtml.isNotEmpty()) { return FeedbackViewModel( - feedback.html, + feedbackHtml, gcsEntityId, hasConversationView, isSplitView.get()!!, @@ -849,10 +866,12 @@ class StatePlayerRecyclerViewAssembler private constructor( private val resourceBucketName: String, private val entityType: String, private val fragment: Fragment, + private val profileId: ProfileId, private val context: Context, private val interactionViewModelFactoryMap: Map, private val backgroundCoroutineDispatcher: CoroutineDispatcher, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) { private val adapterBuilder = BindableAdapter.MultiTypeBuilder.newBuilder( StateItemViewModel::viewType @@ -1300,6 +1319,7 @@ class StatePlayerRecyclerViewAssembler private constructor( /* rhsAdapter= */ adapterBuilder.build(), playerFeatureSet, fragment, + profileId, context, congratulationsTextView, congratulationsTextConfettiView, @@ -1314,7 +1334,8 @@ class StatePlayerRecyclerViewAssembler private constructor( interactionViewModelFactoryMap, backgroundCoroutineDispatcher, hasConversationView, - resourceHandler + resourceHandler, + translationController ) if (playerFeatureSet.conceptCardSupport) { customTagListener.proxyListener = assembler @@ -1330,22 +1351,25 @@ class StatePlayerRecyclerViewAssembler private constructor( private val interactionViewModelFactoryMap: Map< String, @JvmSuppressWildcards InteractionViewModelFactory>, @BackgroundDispatcher private val backgroundCoroutineDispatcher: CoroutineDispatcher, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) { /** * Returns a new [Builder] for the specified GCS resource bucket information for loading - * assets. + * assets, and the current logged in [ProfileId]. */ - fun create(resourceBucketName: String, entityType: String): Builder { + fun create(resourceBucketName: String, entityType: String, profileId: ProfileId): Builder { return Builder( htmlParserFactory, resourceBucketName, entityType, fragment, + profileId, context, interactionViewModelFactoryMap, backgroundCoroutineDispatcher, - resourceHandler + resourceHandler, + translationController ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt index a59acca203a..dbabbbd32ee 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt @@ -2,6 +2,7 @@ package org.oppia.android.app.player.state.itemviewmodel 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.InteractionAnswerHandler import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.android.app.player.state.listener.PreviousNavigationButtonListener @@ -21,21 +22,21 @@ class ContinueInteractionViewModel( val hasConversationView: Boolean, val hasPreviousButton: Boolean, val previousNavigationButtonListener: PreviousNavigationButtonListener, - val isSplitView: Boolean + val isSplitView: Boolean, + private val writtenTranslationContext: WrittenTranslationContext ) : StateItemViewModel(ViewType.CONTINUE_INTERACTION), InteractionAnswerHandler { override fun isExplicitAnswerSubmissionRequired(): Boolean = false override fun isAutoNavigating(): Boolean = true - override fun getPendingAnswer(): UserAnswer { - return UserAnswer.newBuilder() - .setAnswer( - InteractionObject.newBuilder().setNormalizedString(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER) - ) - .setPlainAnswer(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER) - .build() - } + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { + answer = InteractionObject.newBuilder().apply { + normalizedString = DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER + }.build() + plainAnswer = DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER + this.writtenTranslationContext = this@ContinueInteractionViewModel.writtenTranslationContext + }.build() fun handleButtonClicked() { interactionAnswerReceiver.onAnswerReadyForSubmission(getPendingAnswer()) 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 fd8cb16eec7..6eb8dee40bc 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 @@ -12,12 +12,14 @@ import org.oppia.android.app.model.StringList 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.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.recyclerview.OnDragEndedListener import org.oppia.android.app.recyclerview.OnItemDragListener import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.domain.translation.TranslationController /** [StateItemViewModel] for drag drop & sort choice list. */ class DragAndDropSortInteractionViewModel( @@ -26,7 +28,9 @@ class DragAndDropSortInteractionViewModel( interaction: Interaction, private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length val isSplitView: Boolean, - private val resourceHandler: AppLanguageResourceHandler + private val writtenTranslationContext: WrittenTranslationContext, + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : StateItemViewModel(ViewType.DRAG_DROP_SORT_INTERACTION), InteractionAnswerHandler, OnItemDragListener, @@ -44,7 +48,9 @@ class DragAndDropSortInteractionViewModel( private val contentIdHtmlMap: Map = choiceSubtitledHtmls.associate { subtitledHtml -> - subtitledHtml.contentId to subtitledHtml.html + val translatedHtml = + translationController.extractString(subtitledHtml, writtenTranslationContext) + subtitledHtml.contentId to translatedHtml } private val _choiceItems: MutableList = @@ -107,21 +113,19 @@ class DragAndDropSortInteractionViewModel( (adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems) } - override fun getPendingAnswer(): UserAnswer { - val userAnswerBuilder = UserAnswer.newBuilder() + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { val selectedLists = _choiceItems.map { it.htmlContent } val userStringLists = _choiceItems.map { it.computeStringList() } - userAnswerBuilder.listOfHtmlAnswers = convertItemsToAnswer(userStringLists) - userAnswerBuilder.answer = - InteractionObject.newBuilder().apply { - listOfSetsOfTranslatableHtmlContentIds = - ListOfSetsOfTranslatableHtmlContentIds.newBuilder().apply { - _choiceItems.map { } - addAllContentIdLists(selectedLists) - }.build() - }.build() - return userAnswerBuilder.build() - } + listOfHtmlAnswers = convertItemsToAnswer(userStringLists) + answer = InteractionObject.newBuilder().apply { + listOfSetsOfTranslatableHtmlContentIds = + ListOfSetsOfTranslatableHtmlContentIds.newBuilder().apply { + addAllContentIdLists(selectedLists) + }.build() + }.build() + this.writtenTranslationContext = + this@DragAndDropSortInteractionViewModel.writtenTranslationContext + }.build() /** Returns an HTML list containing all of the HTML string elements as items in the list. */ private fun convertItemsToAnswer(htmlItems: List): ListOfSetsOfHtmlStrings { diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt index aae7fe57621..b2e2329cefd 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt @@ -8,11 +8,13 @@ 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.UserAnswer +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.parser.StringToFractionParser 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.translation.AppLanguageResourceHandler +import org.oppia.android.domain.translation.TranslationController /** [StateItemViewModel] for the fraction input interaction. */ class FractionInteractionViewModel( @@ -20,7 +22,9 @@ class FractionInteractionViewModel( val hasConversationView: Boolean, val isSplitView: Boolean, private val errorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, - private val resourceHandler: AppLanguageResourceHandler + private val writtenTranslationContext: WrittenTranslationContext, + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : StateItemViewModel(ViewType.FRACTION_INPUT_INTERACTION), InteractionAnswerHandler { private var pendingAnswerError: String? = null var answerText: CharSequence = "" @@ -44,17 +48,16 @@ class FractionInteractionViewModel( isAnswerAvailable.addOnPropertyChangedCallback(callback) } - override fun getPendingAnswer(): UserAnswer { - val userAnswerBuilder = UserAnswer.newBuilder() + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { if (answerText.isNotEmpty()) { val answerTextString = answerText.toString() - userAnswerBuilder.answer = InteractionObject.newBuilder() - .setFraction(stringToFractionParser.parseFractionFromString(answerTextString)) - .build() - userAnswerBuilder.plainAnswer = answerTextString + answer = InteractionObject.newBuilder().apply { + fraction = stringToFractionParser.parseFractionFromString(answerTextString) + }.build() + plainAnswer = answerTextString + this.writtenTranslationContext = this@FractionInteractionViewModel.writtenTranslationContext } - return userAnswerBuilder.build() - } + }.build() /** It checks the pending error for the current fraction input, and correspondingly updates the error string based on the specified error category. */ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { @@ -94,12 +97,24 @@ class FractionInteractionViewModel( } private fun deriveHintText(interaction: Interaction): CharSequence { - val customPlaceholder = - interaction.customizationArgsMap["customPlaceholder"]?.subtitledUnicode?.unicodeStr ?: "" + // The subtitled unicode can apparently exist in the structure in two different formats. + val placeholderUnicodeOption1 = + interaction.customizationArgsMap["customPlaceholder"]?.subtitledUnicode + val placeholderUnicodeOption2 = + interaction.customizationArgsMap["customPlaceholder"]?.customSchemaValue?.subtitledUnicode + val customPlaceholder1 = + placeholderUnicodeOption1?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" + val customPlaceholder2 = + placeholderUnicodeOption2?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" val allowNonzeroIntegerPart = interaction.customizationArgsMap["allowNonzeroIntegerPart"]?.boolValue ?: true return when { - customPlaceholder.isNotEmpty() -> customPlaceholder + customPlaceholder1.isNotEmpty() -> customPlaceholder1 + customPlaceholder2.isNotEmpty() -> customPlaceholder2 !allowNonzeroIntegerPart -> resourceHandler.getStringInLocale(R.string.fractions_default_hint_text_no_integer) else -> resourceHandler.getStringInLocale(R.string.fractions_default_hint_text) diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt index fac1fe57326..7079d3b10b6 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt @@ -8,6 +8,7 @@ import org.oppia.android.app.model.ImageWithRegions 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.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.translation.AppLanguageResourceHandler @@ -23,6 +24,7 @@ class ImageRegionSelectionInteractionViewModel( interaction: Interaction, private val errorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, val isSplitView: Boolean, + private val writtenTranslationContext: WrittenTranslationContext, private val resourceHandler: AppLanguageResourceHandler ) : StateItemViewModel(ViewType.IMAGE_REGION_SELECTION_INTERACTION), InteractionAnswerHandler, @@ -66,17 +68,18 @@ class ImageRegionSelectionInteractionViewModel( } } - override fun getPendingAnswer(): UserAnswer { - val userAnswerBuilder = UserAnswer.newBuilder() + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { val answerTextString = answerText.toString() - userAnswerBuilder.answer = - InteractionObject.newBuilder().setClickOnImage(parseClickOnImage(answerTextString)).build() - userAnswerBuilder.plainAnswer = resourceHandler.getStringInLocaleWithWrapping( + answer = InteractionObject.newBuilder().apply { + clickOnImage = parseClickOnImage(answerTextString) + }.build() + plainAnswer = resourceHandler.getStringInLocaleWithWrapping( R.string.image_interaction_answer_text, answerTextString ) - return userAnswerBuilder.build() - } + this.writtenTranslationContext = + this@ImageRegionSelectionInteractionViewModel.writtenTranslationContext + }.build() private fun parseClickOnImage(answerTextString: String): ClickOnImage { val region = selectableRegions.find { it.label == answerTextString } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelFactory.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelFactory.kt index 58196c76705..2ad060e78e3 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelFactory.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelFactory.kt @@ -1,6 +1,7 @@ package org.oppia.android.app.player.state.itemviewmodel import org.oppia.android.app.model.Interaction +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver @@ -16,5 +17,6 @@ typealias InteractionViewModelFactory = ( interactionAnswerReceiver: InteractionAnswerReceiver, interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length hasPreviousButton: Boolean, - isSplitView: Boolean + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext ) -> StateItemViewModel diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt index 59007fc6517..32f7d474d12 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt @@ -7,6 +7,7 @@ import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.app.player.state.listener.PreviousNavigationButtonListener import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.domain.translation.TranslationController /** * Module to provide interaction view model-specific dependencies for interactions that should be @@ -25,13 +26,14 @@ class InteractionViewModelModule { @StringKey("Continue") fun provideContinueInteractionViewModelFactory(fragment: Fragment): InteractionViewModelFactory { return { _, hasConversationView, _, interactionAnswerReceiver, _, hasPreviousButton, - isSplitView -> + isSplitView, writtenTranslationContext -> ContinueInteractionViewModel( interactionAnswerReceiver, hasConversationView, hasPreviousButton, fragment as PreviousNavigationButtonListener, - isSplitView + isSplitView, + writtenTranslationContext ) } } @@ -39,16 +41,20 @@ class InteractionViewModelModule { @Provides @IntoMap @StringKey("MultipleChoiceInput") - fun provideMultipleChoiceInputViewModelFactory(): InteractionViewModelFactory { + fun provideMultipleChoiceInputViewModelFactory( + translationController: TranslationController + ): InteractionViewModelFactory { return { entityId, hasConversationView, interaction, interactionAnswerReceiver, - interactionAnswerErrorReceiver, _, isSplitView -> + interactionAnswerErrorReceiver, _, isSplitView, writtenTranslationContext -> SelectionInteractionViewModel( entityId, hasConversationView, interaction, interactionAnswerReceiver, interactionAnswerErrorReceiver, - isSplitView + isSplitView, + writtenTranslationContext, + translationController ) } } @@ -56,16 +62,20 @@ class InteractionViewModelModule { @Provides @IntoMap @StringKey("ItemSelectionInput") - fun provideItemSelectionInputViewModelFactory(): InteractionViewModelFactory { + fun provideItemSelectionInputViewModelFactory( + translationController: TranslationController + ): InteractionViewModelFactory { return { entityId, hasConversationView, interaction, interactionAnswerReceiver, - interactionAnswerErrorReceiver, _, isSplitView -> + interactionAnswerErrorReceiver, _, isSplitView, writtenTranslationContext -> SelectionInteractionViewModel( entityId, hasConversationView, interaction, interactionAnswerReceiver, interactionAnswerErrorReceiver, - isSplitView + isSplitView, + writtenTranslationContext, + translationController ) } } @@ -74,16 +84,19 @@ class InteractionViewModelModule { @IntoMap @StringKey("FractionInput") fun provideFractionInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler + resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ): InteractionViewModelFactory { return { _, hasConversationView, interaction, _, interactionAnswerErrorReceiver, _, - isSplitView -> + isSplitView, writtenTranslationContext -> FractionInteractionViewModel( interaction, hasConversationView, isSplitView, interactionAnswerErrorReceiver, - resourceHandler + writtenTranslationContext, + resourceHandler, + translationController ) } } @@ -94,11 +107,13 @@ class InteractionViewModelModule { fun provideNumericInputViewModelFactory( resourceHandler: AppLanguageResourceHandler ): InteractionViewModelFactory { - return { _, hasConversationView, _, _, interactionAnswerErrorReceiver, _, isSplitView -> + return { _, hasConversationView, _, _, interactionAnswerErrorReceiver, _, isSplitView, + writtenTranslationContext -> NumericInputViewModel( hasConversationView, interactionAnswerErrorReceiver, isSplitView, + writtenTranslationContext, resourceHandler ) } @@ -107,11 +122,14 @@ class InteractionViewModelModule { @Provides @IntoMap @StringKey("TextInput") - fun provideTextInputViewModelFactory(): InteractionViewModelFactory { + fun provideTextInputViewModelFactory( + translationController: TranslationController + ): InteractionViewModelFactory { return { _, hasConversationView, interaction, _, interactionAnswerErrorReceiver, _, - isSplitView -> + isSplitView, writtenTranslationContext -> TextInputViewModel( - interaction, hasConversationView, interactionAnswerErrorReceiver, isSplitView + interaction, hasConversationView, interactionAnswerErrorReceiver, isSplitView, + writtenTranslationContext, translationController ) } } @@ -120,13 +138,14 @@ class InteractionViewModelModule { @IntoMap @StringKey("DragAndDropSortInput") fun provideDragAndDropSortInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler + resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ): InteractionViewModelFactory { return { entityId, hasConversationView, interaction, _, interactionAnswerErrorReceiver, _, - isSplitView -> + isSplitView, writtenTranslationContext -> DragAndDropSortInteractionViewModel( entityId, hasConversationView, interaction, interactionAnswerErrorReceiver, isSplitView, - resourceHandler + writtenTranslationContext, resourceHandler, translationController ) } } @@ -137,13 +156,15 @@ class InteractionViewModelModule { fun provideImageClickInputViewModelFactory( resourceHandler: AppLanguageResourceHandler ): InteractionViewModelFactory { - return { entityId, hasConversationView, interaction, _, answerErrorReceiver, _, isSplitView -> + return { entityId, hasConversationView, interaction, _, answerErrorReceiver, _, isSplitView, + writtenTranslationContext -> ImageRegionSelectionInteractionViewModel( entityId, hasConversationView, interaction, answerErrorReceiver, isSplitView, + writtenTranslationContext, resourceHandler ) } @@ -153,15 +174,19 @@ class InteractionViewModelModule { @IntoMap @StringKey("RatioExpressionInput") fun provideRatioExpressionInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler + resourceHandler: AppLanguageResourceHandler, + translationController: TranslationController ): InteractionViewModelFactory { - return { _, hasConversationView, interaction, _, answerErrorReceiver, _, isSplitView -> + return { _, hasConversationView, interaction, _, answerErrorReceiver, _, isSplitView, + writtenTranslationContext -> RatioExpressionInputInteractionViewModel( interaction, hasConversationView, isSplitView, answerErrorReceiver, - resourceHandler + writtenTranslationContext, + resourceHandler, + translationController ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt index 01bc68eca04..4c34453e937 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt @@ -6,6 +6,7 @@ import androidx.databinding.Observable import androidx.databinding.ObservableField 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.parser.StringToNumberParser import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver @@ -17,6 +18,7 @@ class NumericInputViewModel( val hasConversationView: Boolean, private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length val isSplitView: Boolean, + private val writtenTranslationContext: WrittenTranslationContext, private val resourceHandler: AppLanguageResourceHandler ) : StateItemViewModel(ViewType.NUMERIC_INPUT_INTERACTION), InteractionAnswerHandler { var answerText: CharSequence = "" @@ -74,14 +76,14 @@ class NumericInputViewModel( } } - override fun getPendingAnswer(): UserAnswer { - val userAnswerBuilder = UserAnswer.newBuilder() + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { if (answerText.isNotEmpty()) { val answerTextString = answerText.toString() - userAnswerBuilder.answer = - InteractionObject.newBuilder().setReal(answerTextString.toDouble()).build() - userAnswerBuilder.plainAnswer = answerTextString + answer = InteractionObject.newBuilder().apply { + real = answerTextString.toDouble() + }.build() + plainAnswer = answerTextString + this.writtenTranslationContext = this@NumericInputViewModel.writtenTranslationContext } - return userAnswerBuilder.build() - } + }.build() } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt index 768ff05f560..064b7fc3f60 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt @@ -8,12 +8,14 @@ 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.UserAnswer +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.parser.StringToRatioParser 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.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.toAccessibleAnswerString +import org.oppia.android.domain.translation.TranslationController import org.oppia.android.domain.util.toAnswerString /** [StateItemViewModel] for the ratio expression input interaction. */ @@ -22,7 +24,9 @@ class RatioExpressionInputInteractionViewModel( val hasConversationView: Boolean, val isSplitView: Boolean, private val errorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, - private val resourceHandler: AppLanguageResourceHandler + private val writtenTranslationContext: WrittenTranslationContext, + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController ) : StateItemViewModel(ViewType.RATIO_EXPRESSION_INPUT_INTERACTION), InteractionAnswerHandler { private var pendingAnswerError: String? = null var answerText: CharSequence = "" @@ -48,18 +52,18 @@ class RatioExpressionInputInteractionViewModel( isAnswerAvailable.addOnPropertyChangedCallback(callback) } - override fun getPendingAnswer(): UserAnswer { - val userAnswerBuilder = UserAnswer.newBuilder() + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { if (answerText.isNotEmpty()) { val ratioAnswer = stringToRatioParser.parseRatioOrThrow(answerText.toString()) - userAnswerBuilder.answer = InteractionObject.newBuilder() - .setRatioExpression(ratioAnswer) - .build() - userAnswerBuilder.plainAnswer = ratioAnswer.toAnswerString() - userAnswerBuilder.contentDescription = ratioAnswer.toAccessibleAnswerString(resourceHandler) + answer = InteractionObject.newBuilder().apply { + ratioExpression = ratioAnswer + }.build() + plainAnswer = ratioAnswer.toAnswerString() + contentDescription = ratioAnswer.toAccessibleAnswerString(resourceHandler) + this.writtenTranslationContext = + this@RatioExpressionInputInteractionViewModel.writtenTranslationContext } - return userAnswerBuilder.build() - } + }.build() /** It checks the pending error for the current ratio input, and correspondingly updates the error string based on the specified error category. */ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { @@ -101,10 +105,22 @@ class RatioExpressionInputInteractionViewModel( } private fun deriveHintText(interaction: Interaction): CharSequence { - val placeholder = - interaction.customizationArgsMap["placeholder"]?.subtitledUnicode?.unicodeStr ?: "" + // The subtitled unicode can apparently exist in the structure in two different formats. + val placeholderUnicodeOption1 = + interaction.customizationArgsMap["placeholder"]?.subtitledUnicode + val placeholderUnicodeOption2 = + interaction.customizationArgsMap["placeholder"]?.customSchemaValue?.subtitledUnicode + val placeholder1 = + placeholderUnicodeOption1?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" + val placeholder2 = + placeholderUnicodeOption2?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" return when { - placeholder.isNotEmpty() -> placeholder + placeholder1.isNotEmpty() -> placeholder1 + placeholder2.isNotEmpty() -> placeholder2 else -> resourceHandler.getStringInLocale(R.string.ratio_default_hint_text) } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt index 9a26bac0524..1254f9fbfb5 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -9,10 +9,12 @@ import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds 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.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.android.app.viewmodel.ObservableArrayList +import org.oppia.android.domain.translation.TranslationController /** Corresponds to the type of input that should be used for an item selection interaction view. */ enum class SelectionItemInputType { @@ -27,7 +29,9 @@ class SelectionInteractionViewModel( interaction: Interaction, private val interactionAnswerReceiver: InteractionAnswerReceiver, private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length - val isSplitView: Boolean + val isSplitView: Boolean, + val writtenTranslationContext: WrittenTranslationContext, + private val translationController: TranslationController ) : StateItemViewModel(ViewType.SELECTION_INTERACTION), InteractionAnswerHandler { private val interactionId: String = interaction.id @@ -71,11 +75,14 @@ class SelectionInteractionViewModel( return maxAllowableSelectionCount > 1 } - override fun getPendingAnswer(): UserAnswer { - val userAnswerBuilder = UserAnswer.newBuilder() + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { + val translationContext = this@SelectionInteractionViewModel.writtenTranslationContext val selectedItemSubtitledHtmls = selectedItems.map(choiceItems::get).map { it.htmlContent } + val itemHtmls = selectedItemSubtitledHtmls.map { subtitledHtml -> + translationController.extractString(subtitledHtml, translationContext) + } if (interactionId == "ItemSelectionInput") { - userAnswerBuilder.answer = InteractionObject.newBuilder().apply { + answer = InteractionObject.newBuilder().apply { setOfTranslatableHtmlContentIds = SetOfTranslatableHtmlContentIds.newBuilder().apply { addAllContentIds( selectedItemSubtitledHtmls.map { subtitledHtml -> @@ -86,23 +93,23 @@ class SelectionInteractionViewModel( ) }.build() }.build() - userAnswerBuilder.htmlAnswer = convertSelectedItemsToHtmlString(selectedItemSubtitledHtmls) + htmlAnswer = convertSelectedItemsToHtmlString(itemHtmls) } else if (selectedItems.size == 1) { - userAnswerBuilder.answer = - InteractionObject.newBuilder().setNonNegativeInt(selectedItems.first()).build() - userAnswerBuilder.htmlAnswer = convertSelectedItemsToHtmlString(selectedItemSubtitledHtmls) + answer = InteractionObject.newBuilder().apply { + nonNegativeInt = selectedItems.first() + }.build() + htmlAnswer = convertSelectedItemsToHtmlString(itemHtmls) } - return userAnswerBuilder.build() - } + writtenTranslationContext = translationContext + }.build() /** Returns an HTML list containing all of the HTML string elements as items in the list. */ - private fun convertSelectedItemsToHtmlString(subtitledHtmls: Collection): String { - return when (subtitledHtmls.size) { + private fun convertSelectedItemsToHtmlString(itemHtmls: Collection): String { + return when (itemHtmls.size) { 0 -> "" - 1 -> subtitledHtmls.first().html + 1 -> itemHtmls.first() else -> { - val htmlList = subtitledHtmls.map { it.html } - "
  • ${htmlList.joinToString(separator = "
  • ")}
" + "
  • ${itemHtmls.joinToString(separator = "
  • ")}
" } } } 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 db5e2711d60..821faa8676d 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 @@ -7,15 +7,19 @@ import androidx.databinding.ObservableField 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.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.android.domain.translation.TranslationController /** [StateItemViewModel] for the text input interaction. */ class TextInputViewModel( interaction: Interaction, val hasConversationView: Boolean, private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length - val isSplitView: Boolean + val isSplitView: Boolean, + private val writtenTranslationContext: WrittenTranslationContext, + private val translationController: TranslationController ) : StateItemViewModel(ViewType.TEXT_INPUT_INTERACTION), InteractionAnswerHandler { var answerText: CharSequence = "" val hintText: CharSequence = deriveHintText(interaction) @@ -53,19 +57,31 @@ class TextInputViewModel( } } - override fun getPendingAnswer(): UserAnswer { - val userAnswerBuilder = UserAnswer.newBuilder() + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { if (answerText.isNotEmpty()) { val answerTextString = answerText.toString() - userAnswerBuilder.answer = - InteractionObject.newBuilder().setNormalizedString(answerTextString).build() - userAnswerBuilder.plainAnswer = answerTextString + answer = InteractionObject.newBuilder().apply { + normalizedString = answerTextString + }.build() + plainAnswer = answerTextString + writtenTranslationContext = this@TextInputViewModel.writtenTranslationContext } - return userAnswerBuilder.build() - } + }.build() private fun deriveHintText(interaction: Interaction): CharSequence { - // The default placeholder for text input is empty. - return interaction.customizationArgsMap["placeholder"]?.subtitledUnicode?.unicodeStr ?: "" + // The subtitled unicode can apparently exist in the structure in two different formats. + val placeholderUnicodeOption1 = + interaction.customizationArgsMap["placeholder"]?.subtitledUnicode + val placeholderUnicodeOption2 = + interaction.customizationArgsMap["placeholder"]?.customSchemaValue?.subtitledUnicode + val placeholder1 = + placeholderUnicodeOption1?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" + val placeholder2 = + placeholderUnicodeOption2?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" // The default placeholder for text input is empty. + return if (placeholder1.isNotEmpty()) placeholder1 else placeholder2 } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt index 7a02f1fde96..8f677ae302e 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivity.kt @@ -11,6 +11,7 @@ import org.oppia.android.app.hintsandsolution.RevealHintListener import org.oppia.android.app.hintsandsolution.RevealSolutionInterface import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.State +import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.audio.AudioButtonListener import org.oppia.android.app.player.exploration.HintsAndSolutionExplorationManagerListener import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_DIALOG @@ -44,6 +45,7 @@ class StateFragmentTestActivity : @Inject lateinit var stateFragmentTestActivityPresenter: StateFragmentTestActivityPresenter private lateinit var state: State + private lateinit var writtenTranslationContext: WrittenTranslationContext override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -106,7 +108,8 @@ class StateFragmentTestActivity : HintsAndSolutionDialogFragment.newInstance( explorationId, state, - helpIndex + helpIndex, + writtenTranslationContext ) hintsAndSolutionFragment.showNow(supportFragmentManager, TAG_HINTS_AND_SOLUTION_DIALOG) } @@ -120,8 +123,12 @@ class StateFragmentTestActivity : stateFragmentTestActivityPresenter.revealSolution() } - override fun onExplorationStateLoaded(state: State) { + override fun onExplorationStateLoaded( + state: State, + writtenTranslationContext: WrittenTranslationContext + ) { this.state = state + this.writtenTranslationContext = writtenTranslationContext } private fun getHintsAndSolution(): HintsAndSolutionDialogFragment? { diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt index 92fbf4c80eb..3ca8d4771b4 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt @@ -61,14 +61,6 @@ class StateFragmentTestActivityPresenter @Inject constructor( activity.findViewById