From 5801df02317cc3f052d79a3dea8b3becd06bc60d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 6 Oct 2021 14:11:33 -0700 Subject: [PATCH] Fix #3729, #3730, #3777, #91, #20, part of #3625: Introduce support for localizing lesson content & multi-lingual answer submission (#3796) * Add support for AABs, build flavors, and proguard. There are a lot of details to cover here--see the PR for the complete context. * Lint & codeowner fixes. * Fix failures. - Add missing codeowner - Add support for configuring base branch reference - Update CI for dev/alpha AAB builds to use 'develop' since there's no origin configured by default in the workflows * Different attempt to fix bad develop reference in CI. * Initial commit. This is needed to open a PR on GitHub. This commit is being made so that the PR can start off in a broken Actions state. This also initially disables most non-Bazel workflows to make workflow iteration faster and less impacting on other team members. * Introduce infrastructure for batching. This introduces a new mechanism for passing lists of tests to sharded test targets in CI, and hooks it up. No actual sharding is occurring yet. This led to some simplifications in the CI workflow since the script can be more dynamic in computing the full list of targets (which also works around a previous bug with instrumentation tests being run). Java proto lite also needed to be upgraded for the scripts to be able to use it. More testing/documentation needed as this functionality continues to expand. * Add bucketing strategy. This simply partitions bucketed groups of targets into chunks of 10 for each run. Only 3 buckets are currently retained to test sharding in CI before introducing full support. * Fix caching & stabilize builds. Fixes some caching bucket and output bugs. Also, introduces while loop & keep_going to introduce resilience against app test build failures (or just test failures in general). * Increase sharding & add randomization. Also, enable other workflows. Note that CI shouldn't fully pass yet since some documentation and testing needs to be added yet, but this is meant to be a more realistic test of the CI environment before the PR is finished. * Improving partitionin & readability. Adds a human-readable prefix to make the shards look a bit nicer. Also, adds more fine-tuned partitioning to bucket & reduce shard counts to improve overall timing. Will need to be tested in CI. * Add new tests & fix static analysis errors. * Fix script. A newly computed variable wasn't updated to be used in an earlier change. * Fix broken tests & test configuration. Add docstrings for proto. * Fix mistake from earlier commit. * Try 10 max parallel actions instead. See https://github.com/oppia/oppia-android/pull/3757#issuecomment-911460981 for context. * Fix another error from an earlier commit. * Localisation updates from https://translatewiki.net. * Fix mv command so it works on Linux & OSX. Neither 'mv -t' nor piping to mv work on OSX so we needed to find an alternative (in this case just trying to move everything). This actually works a bit better since it's doing a per-file move rather than accommodating for files that shouldn't be moved (which isn't an issue since the destination directory is different than the one containing the AAB file). * Introduce initial domain layer for translations. Documentation, thorough tests, and detailed description of these changes are still needed. * Initial app layer implementation for translations. This demonstrates working string selection for system-based and overwritten app languages, including necessary activity recreation & layout direction overwriting. This also includes a bunch of Dagger infra refactoring so that some app layer packages can now be modularized (including the new packages). * Domain changes needed per downstream UI changes. * Add patterns & fixes. This involves MANY broad changes to ensure consistent string retrieval (for arrays and plurals), formatting, and string transformations throughout the codebase. Some extra patterns to added to fix things that were needed, and a few issues were fixed along the way. * Add needed domain changes for downstream branch. Also includes fixing circular dependency issue by splitting out some of the locale components to be part of utility rather than domain (so that utiltiy and other packages can depend on MachineLocale). * Introduce support for content localization. This includes a bunch of stuff that'll be described in more detail in the PR description, but it essentially: - Adds support for displaying content in explorations, questions, concept cards, and revision cards in a non-English language - Adds support for submitting non-English answers - Updates test structures to validate everything exception questions is working for localization * Fix structures to work with parsing assumptions. * Fix regex checks for translated strings. Also, performance improvements for the regex check. * Lint-ish fix. * Fix failing regex checks. * Add check for nested res subdirectories. * Clean up locale infra. Add some other needed functionality. * Attempt to delete strings to force history. * Make AAB builds/runs manual-only targets. * Fix broken tests. * Fix lint issues & add KDocs. Also, abstract ContentLocale for consistency & to disallow direct construction. * Add 6/11 test suites (& placeholders for other 4). Silence one file missing a test suite (since it doesn't need one). Also, some tweaks to the language support definitions. * Add more test suites for domain layers. Included introducing a new general purpose utility for testing data providers + its own test suite. * Introduce wrapper & fake for bidi wrapping. Also, add test version of AssetRepository. Add new placeholder tests & update all tests project-wide to make sure that they build. * Add remaining tests. Included some shadow refactoring, and introducing new test-only resources. * Fix Gradle builds. * Lint fixes. * Resolve remaining incomplete TODOs. * Add new codeowners. * Post-merge fixes. Make all non-app layer targets build (haven't run tests yet). Audited existing bidi wrapping cases & converted strings over to being %s-only. * Fix most test targets (builds). All non-app tests confirmed as passing. * Fix all remaining test builds. Introduce new TestActivity for scaffolding all non-activity tests. * Fix all app layer tests. Add fixes for question player & old answer displaying. Add fix for guaranteed crash on startup after some changes between now & the first build of MR3 (dueu to extra updates in SplashActivityPresenter). * Fix questions & profile issues. * Type specifier pattern & fixes. Address temporary TODO by removing kdoc. * Add missing KDocs. * Boilerplate & TODOs for needed tests. * Add new needed test dep. Required an update to truth proto lite import (due to an incompatible update in the common Truth dep). * Add needed testing coverage. Other miscellaneous fixes needed to support new tests. * Two fixes. 1. Introduce proper API compatibility for LocaleController 2. Ensure TranslationController is scoped (breaks test in downstream PR) * Fix Gradle builds on branch. * Resolve nearly all pending TODOs. Only remainder is a test suite whose tests need to be migrated. * Lint fixes. * Re-add method removed from merge. * Lint fixes. This also fixes broken extra/unused imports from the merge. Verified that the dev build works as of this commit. Haven't verified anything else. * Fix compute affected tests script. Adds support for very large PR changesets. * Fix failures found on CI. * Fix remaining Gradle failures found in CI. * Fix existing domain + app layer tests. Some reworking was needed in QuestionAssessmentProgressControllerTest. * Post-merge fix. * Gradle Espresso test fix. * Add missing KDocs, remove extra file, and other cleanups. * Lint fixes. * Deflake DataProviderTestMonitorTest. * Address reviewer comments. * Lint fixes. * Fix affected tests from earlier changes. These failures were found from CI test workflows. * Fix remaining Gradle failures. This introduces a proper fallback mechanism for content strings that allows Gradle builds & tests to work properly, and adds more robustness in case misconfigurations actually happen. * Add placeholders for new needed tests. * Fix broken tests. This came from the earlier commit's fix--the suite hadn't been updated. * Add needed tests for new behaviors. * Fix Gradle build & mechanism change failures. * Lint fixes. * Undo inadvertent change to Gradle jvmargs. * Disable most tests on Espresso. * Test fixes + make monitor Espresso-compatible. * Lint fixes. Co-authored-by: translatewiki.net --- .gitignore | 1 + app/BUILD.bazel | 6 +- .../HintsAndSolutionDialogFragment.kt | 15 +- ...HintsAndSolutionDialogFragmentPresenter.kt | 8 +- .../app/hintsandsolution/HintsViewModel.kt | 24 +- .../RecentlyPlayedFragmentPresenter.kt | 4 +- .../player/exploration/ExplorationActivity.kt | 11 +- ...tionExplorationManagerFragmentPresenter.kt | 2 +- ...tsAndSolutionExplorationManagerListener.kt | 3 +- .../player/state/SelectionInteractionView.kt | 24 +- .../player/state/StateFragmentPresenter.kt | 4 +- .../state/StatePlayerRecyclerViewAssembler.kt | 66 +- .../ContinueInteractionViewModel.kt | 19 +- .../DragAndDropSortInteractionViewModel.kt | 34 +- .../FractionInteractionViewModel.kt | 39 +- ...mageRegionSelectionInteractionViewModel.kt | 17 +- .../InteractionViewModelFactory.kt | 4 +- .../InteractionViewModelModule.kt | 69 +- .../itemviewmodel/NumericInputViewModel.kt | 16 +- ...atioExpressionInputInteractionViewModel.kt | 42 +- .../SelectionInteractionViewModel.kt | 37 +- .../state/itemviewmodel/TextInputViewModel.kt | 36 +- .../testing/StateFragmentTestActivity.kt | 11 +- .../StateFragmentTestActivityPresenter.kt | 20 +- .../oppia/android/app/shim/ViewBindingShim.kt | 7 +- .../android/app/shim/ViewBindingShimImpl.kt | 17 +- .../StoryChapterSummaryViewModel.kt | 4 +- .../ConceptCardFragmentTestActivity.kt | 18 +- ...onceptCardFragmentTestActivityPresenter.kt | 7 +- .../InputInteractionViewTestActivity.kt | 18 +- .../android/app/testing/TopicTestActivity.kt | 3 +- .../app/testing/TopicTestActivityForStory.kt | 3 +- .../oppia/android/app/topic/TopicActivity.kt | 3 +- .../topic/conceptcard/ConceptCardFragment.kt | 28 +- .../ConceptCardFragmentPresenter.kt | 27 +- .../topic/conceptcard/ConceptCardViewModel.kt | 24 +- .../lessons/TopicLessonsFragmentPresenter.kt | 4 +- ...olutionQuestionManagerFragmentPresenter.kt | 3 +- ...HintsAndSolutionQuestionManagerListener.kt | 3 +- .../questionplayer/QuestionPlayerActivity.kt | 43 +- .../QuestionPlayerActivityPresenter.kt | 24 +- .../questionplayer/QuestionPlayerFragment.kt | 27 +- .../QuestionPlayerFragmentPresenter.kt | 9 +- .../RevisionCardActivityPresenter.kt | 32 +- .../revisioncard/RevisionCardFragment.kt | 33 +- .../RevisionCardFragmentPresenter.kt | 25 +- .../revisioncard/RevisionCardViewModel.kt | 30 +- .../main/res/layout/concept_card_fragment.xml | 2 +- .../res/layout/selection_interaction_item.xml | 1 + .../android/app/home/HomeActivityTest.kt | 18 + .../exploration/ExplorationActivityTest.kt | 275 ++- .../app/player/state/StateFragmentTest.kt | 1233 ++++++++-- .../app/recyclerview/RecyclerViewMatcher.kt | 9 +- .../android/app/splash/SplashActivityTest.kt | 2 +- .../android/app/topic/TopicActivityTest.kt | 90 +- .../conceptcard/ConceptCardFragmentTest.kt | 130 +- .../practice/TopicPracticeFragmentTest.kt | 3 +- .../QuestionPlayerActivityTest.kt | 134 +- .../revisioncard/RevisionCardActivityTest.kt | 122 +- .../revisioncard/RevisionCardFragmentTest.kt | 204 +- .../player/state/StateFragmentLocalTest.kt | 208 +- .../QuestionPlayerActivityLocalTest.kt | 3 +- .../AppLanguageWatcherMixinTest.kt | 6 +- build_flavors.bzl | 3 + .../languages/supported_languages.textproto | 9 + domain/BUILD.bazel | 6 + domain/src/main/assets/13.json | 278 ++- domain/src/main/assets/13.textproto | 144 ++ domain/src/main/assets/2mzzFVDLuAj8.json | 1206 +++++----- domain/src/main/assets/2mzzFVDLuAj8.textproto | 184 +- domain/src/main/assets/5NWuolNcwH6e.json | 662 +++--- domain/src/main/assets/5NWuolNcwH6e.textproto | 170 +- domain/src/main/assets/GJ2rLXRKD5hw.json | 24 +- domain/src/main/assets/MjZzEVOG47_1.json | 1206 +++++----- domain/src/main/assets/MjZzEVOG47_1.textproto | 30 +- domain/src/main/assets/k2bQ7z5XHNbK.json | 736 +++--- domain/src/main/assets/k2bQ7z5XHNbK.textproto | 62 +- domain/src/main/assets/omzF4oqgeTXd.json | 18 +- domain/src/main/assets/omzF4oqgeTXd_1.json | 2 +- domain/src/main/assets/questions.json | 676 +++--- domain/src/main/assets/questions.textproto | 70 +- domain/src/main/assets/skills.json | 308 ++- domain/src/main/assets/skills.textproto | 361 +++ domain/src/main/assets/tIoSb3HZFN6e.json | 602 ++--- domain/src/main/assets/tIoSb3HZFN6e.textproto | 52 +- domain/src/main/assets/test_exp_id_2.json | 1076 +++++++-- .../src/main/assets/test_exp_id_2.textproto | 906 +++++++- domain/src/main/assets/test_exp_id_4.json | 50 +- domain/src/main/assets/test_story_id_0.json | 20 +- domain/src/main/assets/test_story_id_2.json | 10 +- domain/src/main/assets/test_topic_id_0.json | 23 +- .../src/main/assets/test_topic_id_0.textproto | 1 + domain/src/main/assets/test_topic_id_0_1.json | 33 + .../main/assets/test_topic_id_0_1.textproto | 33 + domain/src/main/assets/test_topic_id_1.json | 13 +- domain/src/main/assets/test_topic_id_2.json | 2 +- domain/src/main/assets/umPkwp0L1M0-.json | 2018 ++++++++--------- domain/src/main/assets/umPkwp0L1M0-.textproto | 24 +- domain/src/main/assets/wAMdg4oOClga.json | 20 +- domain/src/main/assets/wANbh4oOClga.json | 20 +- domain/src/main/assets/xBSdg4oOClga.json | 20 +- .../AnswerClassificationController.kt | 26 +- .../android/domain/classify/RuleClassifier.kt | 10 +- .../classify/rules/GenericRuleClassifier.kt | 135 +- ...asElementXAtPositionYClassifierProvider.kt | 8 +- ...lementXBeforeElementYClassifierProvider.kt | 8 +- ...nputIsEqualToOrderingClassifierProvider.kt | 4 +- ...emAtIncorrectPositionClassifierProvider.kt | 4 +- ...enominatorEqualToRuleClassifierProvider.kt | 7 +- ...artExactlyEqualToRuleClassifierProvider.kt | 7 +- ...ntegerPartEqualToRuleClassifierProvider.kt | 7 +- ...sNoFractionalPartRuleClassifierProvider.kt | 6 +- ...sNumeratorEqualToRuleClassifierProvider.kt | 7 +- ...AndInSimplestFormRuleClassifierProvider.kt | 7 +- ...putIsEquivalentToRuleClassifierProvider.kt | 7 +- ...tIsExactlyEqualToRuleClassifierProvider.kt | 7 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 7 +- ...onInputIsLessThanRuleClassifierProvider.kt | 7 +- ...ckInputIsInRegionRuleClassifierProvider.kt | 7 +- ...tainsAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ntainAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ectionInputEqualsRuleClassifierProvider.kt | 4 +- ...tIsProperSubsetOfRuleClassifierProvider.kt | 4 +- ...ChoiceInputEqualsRuleClassifierProvider.kt | 9 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 7 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 7 +- ...umericInputEqualsRuleClassifierProvider.kt | 9 +- ...aterThanOrEqualToRuleClassifierProvider.kt | 9 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 9 +- ...nclusivelyBetweenRuleClassifierProvider.kt | 10 +- ...LessThanOrEqualToRuleClassifierProvider.kt | 9 +- ...icInputIsLessThanRuleClassifierProvider.kt | 9 +- ...IsWithinToleranceRuleClassifierProvider.kt | 10 +- .../RatioInputEqualsRuleClassifierProvider.kt | 9 +- ...sNumberOfTermsEqualToClassifierProvider.kt | 9 +- ...InputIsEquivalentRuleClassifierProvider.kt | 9 +- ...TextInputContainsRuleClassifierProvider.kt | 14 +- .../TextInputEqualsRuleClassifierProvider.kt | 14 +- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 14 +- ...xtInputStartsWithRuleClassifierProvider.kt | 14 +- .../ExplorationProgressController.kt | 66 +- .../oppia/android/domain/locale/BUILD.bazel | 2 +- .../android/domain/locale/LocaleController.kt | 87 +- .../android/domain/oppialogger/BUILD.bazel | 2 +- .../domain/oppialogger/analytics/BUILD.bazel | 2 +- .../domain/oppialogger/exceptions/BUILD.bazel | 2 +- .../oppialogger/loguploader/BUILD.bazel | 4 +- .../question/QuestionAssessmentProgress.kt | 2 + .../QuestionAssessmentProgressController.kt | 64 +- .../question/QuestionTrainingController.kt | 36 +- .../domain/topic/ConceptCardRetriever.kt | 62 +- .../domain/topic/RevisionCardRetriever.kt | 44 + .../android/domain/topic/TopicController.kt | 66 +- .../android/domain/translation/BUILD.bazel | 1 + .../translation/TranslationController.kt | 83 +- .../android/domain/util/StateRetriever.kt | 44 + .../AnswerClassificationControllerTest.kt | 270 ++- .../classify/InteractionObjectTestBuilder.kt | 25 +- ...tXAtPositionYRuleClassifierProviderTest.kt | 31 +- ...eforeElementYRuleClassifierProviderTest.kt | 28 +- ...IsEqualToOrderingClassifierProviderTest.kt | 37 +- ...IncorrectPositionClassifierProviderTest.kt | 19 +- ...inatorEqualToRuleClassifierProviderTest.kt | 19 +- ...xactlyEqualToRuleClassifierProviderTest.kt | 31 +- ...erPartEqualToRuleClassifierProviderTest.kt | 64 +- ...ractionalPartRuleClassifierProviderTest.kt | 25 +- ...eratorEqualToRuleClassifierProviderTest.kt | 22 +- ...nSimplestFormRuleClassifierProviderTest.kt | 82 +- ...sEquivalentToRuleClassifierProviderTest.kt | 40 +- ...xactlyEqualToRuleClassifierProviderTest.kt | 37 +- ...IsGreaterThanRuleClassifierProviderTest.kt | 79 +- ...putIsLessThanRuleClassifierProviderTest.kt | 79 +- ...putIsInRegionRuleClassifierProviderTest.kt | 25 +- ...sAtLeastOneOfRuleClassifierProviderTest.kt | 19 +- ...nAtLeastOneOfRuleClassifierProviderTest.kt | 34 +- ...onInputEqualsRuleClassifierProviderTest.kt | 28 +- ...roperSubsetOfRuleClassifierProviderTest.kt | 37 +- ...ceInputEqualsRuleClassifierProviderTest.kt | 16 +- ...nitsIsEqualToRuleClassifierProviderTest.kt | 22 +- ...sEquivalentToRuleClassifierProviderTest.kt | 25 +- ...icInputEqualsRuleClassifierProviderTest.kt | 49 +- ...ThanOrEqualToRuleClassifierProviderTest.kt | 85 +- ...IsGreaterThanRuleClassifierProviderTest.kt | 84 +- ...sivelyBetweenRuleClassifierProviderTest.kt | 127 +- ...ThanOrEqualToRuleClassifierProviderTest.kt | 99 +- ...putIsLessThanRuleClassifierProviderTest.kt | 99 +- ...thinToleranceRuleClassifierProviderTest.kt | 212 +- ...ioInputEqualsRuleClassifierProviderTest.kt | 16 +- ...berOfTermsEqualToClassifierProviderTest.kt | 13 +- ...tIsEquivalentRuleClassifierProviderTest.kt | 25 +- ...InputContainsRuleClassifierProviderTest.kt | 183 +- ...xtInputEqualsRuleClassifierProviderTest.kt | 172 +- ...utFuzzyEqualsRuleClassifierProviderTest.kt | 202 +- ...putStartsWithRuleClassifierProviderTest.kt | 175 +- .../ExplorationProgressControllerTest.kt | 1344 +++++------ .../locale/LanguageConfigRetrieverTest.kt | 4 +- .../domain/locale/LocaleControllerTest.kt | 51 +- ...aughtExceptionLoggerStartupListenerTest.kt | 24 +- .../LogUploadWorkManagerInitializerTest.kt | 24 +- .../loguploader/LogUploadWorkerTest.kt | 24 +- ...uestionAssessmentProgressControllerTest.kt | 808 ++++--- .../QuestionTrainingControllerTest.kt | 77 +- .../domain/topic/TopicControllerTest.kt | 476 ++-- .../android/domain/translation/BUILD.bazel | 1 + .../translation/TranslationControllerTest.kt | 374 ++- .../android/domain/util/StateRetrieverTest.kt | 24 + model/src/main/proto/exploration.proto | 9 + model/src/main/proto/topic.proto | 17 + model/src/main/proto/translation.proto | 20 +- oppia_android_application.bzl | 2 +- .../scripts/ci/ComputeAffectedTests.kt | 16 +- .../oppia/android/testing/data/BUILD.bazel | 1 + .../testing/data/DataProviderTestMonitor.kt | 9 +- .../junit/DefineAppLanguageLocaleContext.kt | 14 + .../junit/InitializeDefaultLocaleRule.kt | 23 +- utility/BUILD.bazel | 3 +- utility/build.gradle | 1 + .../util/data/AsyncDataSubscriptionManager.kt | 198 +- .../org/oppia/android/util/data/BUILD.bazel | 1 - .../util/extensions/BundleExtensions.kt | 21 + .../util/locale/AndroidLocaleProfile.kt | 15 + .../oppia/android/util/threading/BUILD.bazel | 8 - .../util/threading/ConcurrentCollections.kt | 36 - .../data/AsyncDataSubscriptionManagerTest.kt | 566 +++++ .../util/extensions/BundleExtensionsTest.kt | 88 +- .../util/locale/AndroidLocaleProfileTest.kt | 58 + 226 files changed, 15123 insertions(+), 7296 deletions(-) create mode 100644 domain/src/main/assets/test_topic_id_0_1.json create mode 100644 domain/src/main/assets/test_topic_id_0_1.textproto delete mode 100644 utility/src/main/java/org/oppia/android/util/threading/ConcurrentCollections.kt create mode 100644 utility/src/test/java/org/oppia/android/util/data/AsyncDataSubscriptionManagerTest.kt 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