diff --git a/WORKSPACE b/WORKSPACE index 0c7c57e3f8c..a79cf09e469 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -129,6 +129,15 @@ git_repository( remote = "https://github.com/oppia/androidsvg", ) +# A custom fork of KotliTeX that removes resources artifacts that break the build, and updates the +# min target SDK version to be compatible with Oppia. +git_repository( + name = "kotlitex", + commit = "108a12fd8743a09936d614ca7d012c8f079bad62", + remote = "https://github.com/oppia/kotlitex", + shallow_since = "1647554845 -0700", +) + bind( name = "databinding_annotation_processor", actual = "//tools/android:compiler_annotation_processor", diff --git a/app/BUILD.bazel b/app/BUILD.bazel index ff5bb845c0f..e6bef34f8cd 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -102,6 +102,7 @@ LISTENERS = [ "src/main/java/org/oppia/android/app/devoptions/RouteToMarkChaptersCompletedListener.kt", "src/main/java/org/oppia/android/app/devoptions/RouteToMarkStoriesCompletedListener.kt", "src/main/java/org/oppia/android/app/devoptions/RouteToMarkTopicsCompletedListener.kt", + "src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt", "src/main/java/org/oppia/android/app/devoptions/RouteToViewEventLogsListener.kt", "src/main/java/org/oppia/android/app/drawer/RouteToProfileProgressListener.kt", "src/main/java/org/oppia/android/app/help/LoadFaqListFragmentListener.kt", @@ -182,6 +183,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileListViewModel.kt", "src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/SyncStatusItemViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeViewModel.kt", + "src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt", "src/main/java/org/oppia/android/app/drawer/NavigationDrawerHeaderViewModel.kt", "src/main/java/org/oppia/android/app/help/HelpItemViewModel.kt", "src/main/java/org/oppia/android/app/help/HelpListViewModel.kt", @@ -202,12 +204,13 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt", "src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt", "src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt", - "src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt", + "src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt", "src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt", "src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt", + "src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/PreviousResponsesHeaderViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/SubmittedAnswerViewModel.kt", @@ -248,6 +251,7 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsItemViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsModifyLessonProgressViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsOverrideAppBehaviorsViewModel.kt", + "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsViewLogsViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt", "src/main/java/org/oppia/android/app/devoptions/forcenetworktype/NetworkTypeItemViewModel.kt", @@ -396,6 +400,7 @@ VIEWS_WITH_RESOURCE_IMPORTS = [ VIEWS = [ "src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/CopyIdMaterialButtonView.kt", "src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt", + "src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt", "src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt", "src/main/java/org/oppia/android/app/customview/interaction/TextInputInteractionView.kt", "src/main/java/org/oppia/android/app/customview/interaction/RatioInputInteractionView.kt", @@ -552,6 +557,7 @@ android_library( resource_files = glob(DATABINDING_LAYOUTS), visibility = [ "//app/src/main/java/org/oppia/android/app/shim:__pkg__", + "//app/src/main/java/org/oppia/android/app/testing/activity:__pkg__", ], deps = [ ":annotations", @@ -561,8 +567,8 @@ android_library( ":views", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:interaction_object_java_proto_lite", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_core_core", @@ -593,8 +599,8 @@ kt_android_library( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_recyclerview_recyclerview", ], ) @@ -662,6 +668,7 @@ kt_android_library( "//app/src/main/java/org/oppia/android/app/viewmodel:observable_view_model", "//app/src/main/java/org/oppia/android/app/viewmodel:view_model_provider", "//app/src/main/java/org/oppia/android/app/utility/datetime:date_time_util", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/clipboard:clipboard_controller", @@ -674,9 +681,11 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_event_logger", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", # TODO(#59): Remove 'debug_util_module' once we completely migrate to Bazel from Gradle as # we can then directly exclude debug files from the build and thus won't be requiring this module. "//utility/src/main/java/org/oppia/android/util/networking:debug_util_module", + "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser", ], ) @@ -702,7 +711,7 @@ android_library( ":view_models", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_lifecycle_lifecycle-livedata-core", @@ -748,13 +757,16 @@ kt_android_library( "//data/src/main/java/org/oppia/android/data/backends/gae:network_config_prod_module", "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -765,8 +777,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", + "//model/src/main/proto:arguments_java_proto_lite", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", - "//model:arguments_java_proto_lite", "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:prod_module", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//third_party:androidx_databinding_databinding-adapters", @@ -795,7 +807,9 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_module", # TODO(#2432): Replace debug_module with prod_module when building the app in prod mode. "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger_injector_provider", "//utility/src/main/java/org/oppia/android/util/statusbar:status_bar_color", + "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector_provider", ], ) @@ -823,7 +837,10 @@ kt_android_library( "src/sharedTest/java/org/oppia/android/app/utility/ProgressMatcher.kt", "src/sharedTest/java/org/oppia/android/app/utility/TabMatcher.kt", ], - visibility = ["//app:__subpackages__"], + visibility = [ + ":app_testing_visibility", + "//app:__subpackages__", + ], deps = [ ":app", "//testing", @@ -839,15 +856,19 @@ TEST_DEPS = [ ":test_deps", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -861,6 +882,8 @@ TEST_DEPS = [ "//testing/src/main/java/org/oppia/android/testing/espresso:konfetti_view_matcher", "//testing/src/main/java/org/oppia/android/testing/espresso:text_input_action", "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", @@ -896,6 +919,7 @@ TEST_DEPS = [ "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", "//utility/src/main/java/org/oppia/android/util/parser/html:custom_bullet_span", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser", "//utility/src/main/java/org/oppia/android/util/parser/html:html_parser_entity_type_module", diff --git a/app/build.gradle b/app/build.gradle index 1b784b21cc2..b2948118e1a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,8 +136,8 @@ dependencies { 'de.hdodenhof:circleimageview:3.0.1', 'nl.dionsegijn:konfetti:1.2.5', "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version", - 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1', - 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1', + 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1', + 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1', 'org.mockito:mockito-core:2.7.22', ) implementation 'com.android.support.constraint:constraint-layout:2.0.4' @@ -172,6 +172,7 @@ dependencies { 'androidx.test.ext:junit:1.1.1', 'com.github.bumptech.glide:mocks:4.11.0', 'com.google.truth:truth:1.1.3', + 'com.google.truth.extensions:truth-liteproto-extension:1.1.3', 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2', 'org.mockito:mockito-android:2.7.22', 'org.robolectric:annotations:4.5', diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fd6cbd8640c..1d8ddc7de6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -270,6 +270,10 @@ + ): DeviceSettings { - if (deviceSettingsResult.isFailure()) { - oppiaLogger.e( - "AdministratorControlsFragment", - "Failed to retrieve profile", - deviceSettingsResult.getErrorOrNull()!! - ) + return when (deviceSettingsResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "AdministratorControlsFragment", "Failed to retrieve profile", deviceSettingsResult.error + ) + DeviceSettings.getDefaultInstance() + } + is AsyncResult.Pending -> DeviceSettings.getDefaultInstance() + is AsyncResult.Success -> deviceSettingsResult.value } - return deviceSettingsResult.getOrDefault(DeviceSettings.getDefaultInstance()) } private fun processAdministratorControlsList( diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt index 8dded74ea84..861862fe597 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt @@ -7,6 +7,7 @@ import org.oppia.android.app.model.DeviceSettings import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData /** [ViewModel] for the recycler view in [AdministratorControlsFragment]. */ @@ -31,11 +32,11 @@ class AdministratorControlsDownloadPermissionsViewModel( .observe( fragment, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( "AdministratorControlsFragment", "Failed to update topic update on wifi permission", - it.getErrorOrNull()!! + it.error ) } } @@ -49,11 +50,11 @@ class AdministratorControlsDownloadPermissionsViewModel( ).toLiveData().observe( fragment, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( "AdministratorControlsFragment", "Failed to update topic auto update permission", - it.getErrorOrNull()!! + it.error ) } } diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt index f89ed619539..a19ba19a24a 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt @@ -7,6 +7,7 @@ import dagger.Component import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.IntentFactoryShimModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.topic.PracticeTabModule @@ -14,13 +15,16 @@ import org.oppia.android.app.translation.ActivityRecreatorProdModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -96,8 +100,10 @@ import javax.inject.Singleton ExplorationStorageModule::class, DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class, NetworkConfigProdModule::class, AssetModule::class, - LocaleProdModule::class, ActivityRecreatorProdModule::class, LoggingIdentifierModule::class, - ApplicationLifecycleModule::class, UserIdProdModule::class, + LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, UserIdProdModule::class, // TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then // directly exclude debug files from the build and thus won't be requiring this module. NetworkConnectionDebugUtilModule::class, LoggingIdentifierModule::class, SyncStatusModule::class diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt index 39717b2ec30..0d27a11aced 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt @@ -3,11 +3,15 @@ package org.oppia.android.app.application import org.oppia.android.app.translation.AppLanguageApplicationInjector import org.oppia.android.domain.locale.LocaleApplicationInjector import org.oppia.android.util.data.DataProvidersInjector +import org.oppia.android.util.logging.ConsoleLoggerInjector import org.oppia.android.util.system.OppiaClockInjector +import org.oppia.android.util.threading.DispatcherInjector /** Injector for application-level dependencies that can't be directly injected where needed. */ interface ApplicationInjector : DataProvidersInjector, AppLanguageApplicationInjector, OppiaClockInjector, - LocaleApplicationInjector + LocaleApplicationInjector, + DispatcherInjector, + ConsoleLoggerInjector diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt index 55c68268d00..4ef6fc55b3a 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt @@ -6,15 +6,21 @@ import org.oppia.android.domain.locale.LocaleApplicationInjector import org.oppia.android.domain.locale.LocaleApplicationInjectorProvider import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider +import org.oppia.android.util.logging.ConsoleLoggerInjector +import org.oppia.android.util.logging.ConsoleLoggerInjectorProvider import org.oppia.android.util.system.OppiaClockInjector import org.oppia.android.util.system.OppiaClockInjectorProvider +import org.oppia.android.util.threading.DispatcherInjector +import org.oppia.android.util.threading.DispatcherInjectorProvider /** Provider for [ApplicationInjector]. The application context will implement this interface. */ interface ApplicationInjectorProvider : DataProvidersInjectorProvider, AppLanguageApplicationInjectorProvider, OppiaClockInjectorProvider, - LocaleApplicationInjectorProvider { + LocaleApplicationInjectorProvider, + DispatcherInjectorProvider, + ConsoleLoggerInjectorProvider { fun getApplicationInjector(): ApplicationInjector override fun getDataProvidersInjector(): DataProvidersInjector = getApplicationInjector() @@ -25,4 +31,8 @@ interface ApplicationInjectorProvider : override fun getOppiaClockInjector(): OppiaClockInjector = getApplicationInjector() override fun getLocaleApplicationInjector(): LocaleApplicationInjector = getApplicationInjector() + + override fun getDispatcherInjector(): DispatcherInjector = getApplicationInjector() + + override fun getConsoleLoggerInjector(): ConsoleLoggerInjector = getApplicationInjector() } diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt index 4db3b84d049..4086552343e 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt @@ -48,14 +48,18 @@ class CompletedStoryListViewModel @Inject constructor( private fun processCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "CompletedStoryListFragment", - "Failed to retrieve CompletedStory list: ", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "CompletedStoryListFragment", + "Failed to retrieve CompletedStory list: ", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun processCompletedStoryList( diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt index 6209a6c269e..49972bda253 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt @@ -20,6 +20,8 @@ import org.oppia.android.app.utility.KeyboardHelper.Companion.showSoftKeyboard // background="@drawable/edit_text_background" // maxLength="200". +// TODO(#4135): Add a dedicated test suite for this class. + /** The custom EditText class for fraction input interaction view. */ class FractionInputInteractionView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt new file mode 100644 index 00000000000..26b606237c0 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt @@ -0,0 +1,76 @@ +package org.oppia.android.app.customview.interaction + +import android.content.Context +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener +import org.oppia.android.app.utility.KeyboardHelper.Companion.hideSoftKeyboard +import org.oppia.android.app.utility.KeyboardHelper.Companion.showSoftKeyboard + +// TODO(#249): These are the attributes which should be defined in XML, that are required for this +// interaction view to work correctly: +// placeholder="Write here." +// inputType="text" +// background="@drawable/edit_text_background" +// maxLength="200". + +/** + * The custom [EditText] class for math expression interactions interaction view. + * + * Note that the hint should be set via [setPlaceholder] to ensure that it's properly initialized if + * using databinding, otherwise setting the hint through android:hint should work fine. + */ +class MathExpressionInteractionsView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = android.R.attr.editTextStyle +) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { + private var hintText: CharSequence + private val stateKeyboardButtonListener: StateKeyboardButtonListener + + init { + onFocusChangeListener = this + hintText = (hint ?: "") + stateKeyboardButtonListener = context as StateKeyboardButtonListener + } + + override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) { + hint = "" + typeface = Typeface.DEFAULT + showSoftKeyboard(v, context) + } else { + hint = hintText + if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) + hideSoftKeyboard(v, context) + } + + override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { + if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { + clearFocus() + } + return super.onKeyPreIme(keyCode, event) + } + + override fun onEditorAction(actionCode: Int) { + if (actionCode == EditorInfo.IME_ACTION_DONE) { + stateKeyboardButtonListener.onEditorAction(EditorInfo.IME_ACTION_DONE) + } + super.onEditorAction(actionCode) + } + + /** + * Sets the current placeholder text used by the view to [placeholderText]. + * + * See the class's KDoc for caveats on how this relates to the text view's hint. + */ + fun setPlaceholder(placeholderText: CharSequence) { + hintText = placeholderText + if (!hasFocus()) { + hint = placeholderText + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt index 0bddc8a73d9..6d7b2a5bc4e 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsActivity.kt @@ -10,6 +10,7 @@ import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeActivit import org.oppia.android.app.devoptions.markchapterscompleted.MarkChaptersCompletedActivity import org.oppia.android.app.devoptions.markstoriescompleted.MarkStoriesCompletedActivity import org.oppia.android.app.devoptions.marktopicscompleted.MarkTopicsCompletedActivity +import org.oppia.android.app.devoptions.mathexpressionparser.MathExpressionParserActivity import org.oppia.android.app.devoptions.vieweventlogs.ViewEventLogsActivity import org.oppia.android.app.drawer.NAVIGATION_PROFILE_ID_ARGUMENT_KEY import org.oppia.android.app.translation.AppLanguageResourceHandler @@ -23,7 +24,8 @@ class DeveloperOptionsActivity : RouteToMarkStoriesCompletedListener, RouteToMarkTopicsCompletedListener, RouteToViewEventLogsListener, - RouteToForceNetworkTypeListener { + RouteToForceNetworkTypeListener, + RouteToMathExpressionParserTestListener { @Inject lateinit var developerOptionsActivityPresenter: DeveloperOptionsActivityPresenter @@ -70,6 +72,10 @@ class DeveloperOptionsActivity : startActivity(ForceNetworkTypeActivity.createForceNetworkTypeActivityIntent(this)) } + override fun routeToMathExpressionParserTest() { + startActivity(MathExpressionParserActivity.createIntent(this)) + } + companion object { /** Function to create intent for DeveloperOptionsActivity */ fun createDeveloperOptionsActivityIntent(context: Context, internalProfileId: Int): Intent { diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt index 258749e66ec..5fab6646fa4 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt @@ -9,12 +9,14 @@ import androidx.recyclerview.widget.LinearLayoutManager import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsItemViewModel import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsModifyLessonProgressViewModel import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsOverrideAppBehaviorsViewModel +import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsTestParsersViewModel import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsViewLogsViewModel import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.databinding.DeveloperOptionsFragmentBinding import org.oppia.android.databinding.DeveloperOptionsModifyLessonProgressViewBinding import org.oppia.android.databinding.DeveloperOptionsOverrideAppBehaviorsViewBinding +import org.oppia.android.databinding.DeveloperOptionsTestParsersViewBinding import org.oppia.android.databinding.DeveloperOptionsViewLogsViewBinding import javax.inject.Inject @@ -72,6 +74,10 @@ class DeveloperOptionsFragmentPresenter @Inject constructor( viewModel.itemIndex.set(2) ViewType.VIEW_TYPE_OVERRIDE_APP_BEHAVIORS } + is DeveloperOptionsTestParsersViewModel -> { + viewModel.itemIndex.set(3) + ViewType.VIEW_TYPE_TEST_PARSERS + } else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } } @@ -93,12 +99,19 @@ class DeveloperOptionsFragmentPresenter @Inject constructor( setViewModel = DeveloperOptionsOverrideAppBehaviorsViewBinding::setViewModel, transformViewModel = { it as DeveloperOptionsOverrideAppBehaviorsViewModel } ) + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_TEST_PARSERS, + inflateDataBinding = DeveloperOptionsTestParsersViewBinding::inflate, + setViewModel = DeveloperOptionsTestParsersViewBinding::setViewModel, + transformViewModel = { it as DeveloperOptionsTestParsersViewModel } + ) .build() } private enum class ViewType { VIEW_TYPE_MODIFY_LESSON_PROGRESS, VIEW_TYPE_VIEW_LOGS, - VIEW_TYPE_OVERRIDE_APP_BEHAVIORS + VIEW_TYPE_OVERRIDE_APP_BEHAVIORS, + VIEW_TYPE_TEST_PARSERS } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt index a1724ce1485..a6ee237ec55 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsItemViewModel import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsModifyLessonProgressViewModel import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsOverrideAppBehaviorsViewModel +import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsTestParsersViewModel import org.oppia.android.app.devoptions.devoptionsitemviewmodel.DeveloperOptionsViewLogsViewModel import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.domain.devoptions.ShowAllHintsAndSolutionController @@ -28,6 +29,8 @@ class DeveloperOptionsViewModel @Inject constructor( activity as RouteToMarkTopicsCompletedListener private val routeToViewEventLogsListener = activity as RouteToViewEventLogsListener private val routeToForceNetworkTypeListener = activity as RouteToForceNetworkTypeListener + private val routeToMathExpressionParserTestListener = + activity as RouteToMathExpressionParserTestListener /** * List of [DeveloperOptionsItemViewModel] used to populate recyclerview of @@ -49,7 +52,8 @@ class DeveloperOptionsViewModel @Inject constructor( forceCrashButtonClickListener, routeToForceNetworkTypeListener, showAllHintsAndSolutionController - ) + ), + DeveloperOptionsTestParsersViewModel(routeToMathExpressionParserTestListener) ) } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt b/app/src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt new file mode 100644 index 00000000000..daf68a254d9 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/RouteToMathExpressionParserTestListener.kt @@ -0,0 +1,7 @@ +package org.oppia.android.app.devoptions + +/** Listener for when the user wants to test math expressions/equations. */ +interface RouteToMathExpressionParserTestListener { + /** Called when the user indicates that they want to test math expressions/equations. */ + fun routeToMathExpressionParserTest() +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt new file mode 100644 index 00000000000..00e7edf8f56 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/devoptionsitemviewmodel/DeveloperOptionsTestParsersViewModel.kt @@ -0,0 +1,16 @@ +package org.oppia.android.app.devoptions.devoptionsitemviewmodel + +import org.oppia.android.app.devoptions.RouteToMathExpressionParserTestListener + +/** + * [DeveloperOptionsItemViewModel] to provide features to test and debug math expressions and + * equations. + */ +class DeveloperOptionsTestParsersViewModel( + private val routeToMathExpressionParserTestListener: RouteToMathExpressionParserTestListener +) : DeveloperOptionsItemViewModel() { + /** Routes the user to an activity for testing math expressions & equations. */ + fun onMathExpressionsClicked() { + routeToMathExpressionParserTestListener.routeToMathExpressionParserTest() + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt index bcc09604a72..24d1b7fc4e7 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt @@ -48,14 +48,16 @@ class MarkChaptersCompletedViewModel @Inject constructor( private fun processStoryMapResult( storyMap: AsyncResult>> ): Map> { - if (storyMap.isFailure()) { - oppiaLogger.e( - "MarkChaptersCompletedFragment", - "Failed to retrieve storyList", - storyMap.getErrorOrNull()!! - ) + return when (storyMap) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkChaptersCompletedFragment", "Failed to retrieve storyList", storyMap.error + ) + mapOf() + } + is AsyncResult.Pending -> mapOf() + is AsyncResult.Success -> storyMap.value } - return storyMap.getOrDefault(mapOf()) } private fun processStoryMap( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt index b105bbc5a31..8ec76b5d69e 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt @@ -48,14 +48,16 @@ class MarkStoriesCompletedViewModel @Inject constructor( private fun processStoryMapResult( storyMap: AsyncResult>> ): Map> { - if (storyMap.isFailure()) { - oppiaLogger.e( - "MarkStoriesCompletedFragment", - "Failed to retrieve storyList", - storyMap.getErrorOrNull()!! - ) + return when (storyMap) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkStoriesCompletedFragment", "Failed to retrieve storyList", storyMap.error + ) + mapOf() + } + is AsyncResult.Pending -> mapOf() + is AsyncResult.Success -> storyMap.value } - return storyMap.getOrDefault(mapOf()) } private fun processStoryMap( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt index ca48064520c..fd2d06e094b 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt @@ -45,14 +45,16 @@ class MarkTopicsCompletedViewModel @Inject constructor( } private fun processAllTopicsResult(allTopics: AsyncResult>): List { - if (allTopics.isFailure()) { - oppiaLogger.e( - "MarkTopicsCompletedFragment", - "Failed to retrieve all topics", - allTopics.getErrorOrNull()!! - ) + return when (allTopics) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkTopicsCompletedFragment", "Failed to retrieve all topics", allTopics.error + ) + mutableListOf() + } + is AsyncResult.Pending -> mutableListOf() + is AsyncResult.Success -> allTopics.value } - return allTopics.getOrDefault(mutableListOf()) } private fun processAllTopics(allTopics: List): List { diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt new file mode 100644 index 00000000000..25349699375 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivity.kt @@ -0,0 +1,33 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAppCompatActivity +import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject + +/** Activity to allow the user to test math expressions/equations. */ +class MathExpressionParserActivity : InjectableAppCompatActivity() { + @Inject + lateinit var mathExpressionParserActivityPresenter: MathExpressionParserActivityPresenter + + @Inject + lateinit var resourceHandler: AppLanguageResourceHandler + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + mathExpressionParserActivityPresenter.handleOnCreate() + title = resourceHandler.getStringInLocale(R.string.math_expression_parser_activity_title) + } + + companion object { + /** Returns [Intent] for [MathExpressionParserActivity]. */ + fun createIntent(context: Context): Intent { + return Intent(context, MathExpressionParserActivity::class.java) + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivityPresenter.kt new file mode 100644 index 00000000000..0d9bcfc1d80 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserActivityPresenter.kt @@ -0,0 +1,33 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import androidx.appcompat.app.AppCompatActivity +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityScope +import javax.inject.Inject + +/** The presenter for [MathExpressionParserActivity]. */ +@ActivityScope +class MathExpressionParserActivityPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + + /** Called when [MathExpressionParserActivity] is created. Handles UI for the activity. */ + fun handleOnCreate() { + activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) + activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_white_24dp) + activity.setContentView(R.layout.math_expression_parser_activity) + + if (getMathExpressionParserFragment() == null) { + val forceNetworkTypeFragment = MathExpressionParserFragment.createNewInstance() + activity.supportFragmentManager.beginTransaction().add( + R.id.math_expression_parser_container, + forceNetworkTypeFragment + ).commitNow() + } + } + + private fun getMathExpressionParserFragment(): MathExpressionParserFragment? { + return activity.supportFragmentManager + .findFragmentById(R.id.force_network_type_container) as? MathExpressionParserFragment + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragment.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragment.kt new file mode 100644 index 00000000000..4675e0eb376 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragment.kt @@ -0,0 +1,34 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import javax.inject.Inject + +/** Fragment to provide user testing support for math expressions/equations. */ +class MathExpressionParserFragment : InjectableFragment() { + @Inject + lateinit var mathExpressionParserFragmentPresenter: MathExpressionParserFragmentPresenter + + companion object { + /** Returns a new instance of [MathExpressionParserFragment]. */ + fun createNewInstance(): MathExpressionParserFragment = MathExpressionParserFragment() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return mathExpressionParserFragmentPresenter.handleCreateView(inflater, container) + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragmentPresenter.kt new file mode 100644 index 00000000000..26439dbd0fc --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserFragmentPresenter.kt @@ -0,0 +1,39 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import org.oppia.android.databinding.MathExpressionParserFragmentBinding +import javax.inject.Inject + +/** The presenter for [MathExpressionParserFragment]. */ +class MathExpressionParserFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val fragment: Fragment, + private val viewModel: MathExpressionParserViewModel +) { + /** Called when [MathExpressionParserFragment] is created. Handles UI for the fragment. */ + fun handleCreateView( + inflater: LayoutInflater, + container: ViewGroup? + ): View { + val binding = MathExpressionParserFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + + binding.mathExpressionParserToolbar.setNavigationOnClickListener { + (activity as MathExpressionParserActivity).finish() + } + + binding.apply { + lifecycleOwner = fragment + viewModel = this@MathExpressionParserFragmentPresenter.viewModel + } + viewModel.initialize(binding.mathExpressionParseResultTextView) + return binding.root + } +} diff --git a/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt new file mode 100644 index 00000000000..741b17ca7aa --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/devoptions/mathexpressionparser/MathExpressionParserViewModel.kt @@ -0,0 +1,228 @@ +package org.oppia.android.app.devoptions.mathexpressionparser + +import android.widget.TextView +import androidx.databinding.ObservableField +import org.oppia.android.R +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil +import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.util.locale.OppiaLocale +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperation +import org.oppia.android.util.math.toPolynomial +import org.oppia.android.util.math.toRawLatex +import org.oppia.android.util.parser.html.HtmlParser +import javax.inject.Inject + +/** + * View model that provides different debugging scenarios for math expressions, equations, and + * numeric expressions. + */ +@FragmentScope +class MathExpressionParserViewModel @Inject constructor( + private val appLanguageResourceHandler: AppLanguageResourceHandler, + private val machineLocale: OppiaLocale.MachineLocale, + private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + private val htmlParserFactory: HtmlParser.Factory +) : ObservableViewModel() { + private val htmlParser by lazy { + // TODO(#4206): Replace this with the variant that doesn't require GCS properties. + htmlParserFactory.create( + gcsResourceName = "", + entityType = "", + entityId = "", + imageCenterAlign = false + ) + } + private lateinit var parseResultTextView: TextView + + /** + * Specifies the math expression currently being entered by the user. This is expected to be + * directly bound to the UI. + */ + var mathExpression = ObservableField() + + /** + * Specifies the comma-separated list of variables allowed for algebraic expressions/equations, as + * specified by the user. This is expected to be directly bound to the UI. + */ + var allowedVariables = ObservableField("x,y") + private var parseType = ParseType.NUMERIC_EXPRESSION + private var resultType = ResultType.MATH_EXPRESSION + private var useDivAsFractions = false + + /** Initializes the view model to use [parseResultTextView] for displaying the parse result. */ + fun initialize(parseResultTextView: TextView) { + this.parseResultTextView = parseResultTextView + updateParseResult() + } + + /** Callback for the UI to recompute the parse result. */ + fun onParseButtonClicked() { + updateParseResult() + } + + /** Callback for the UI to update the current [ParseType] used. */ + fun onParseTypeSelected(parseType: ParseType) { + this.parseType = parseType + } + + /** Callback for the UI to update the current [ResultType] used. */ + fun onResultTypeSelected(resultType: ResultType) { + this.resultType = resultType + } + + /** + * Callback for the UI to update whether divisions should be treated as fractions for relevant + * [ResultType]s. + */ + fun onChangedUseDivAsFractions(useDivAsFractions: Boolean) { + this.useDivAsFractions = useDivAsFractions + } + + private fun updateParseResult() { + val newText = computeParseResult() + // Only parse HTML if there is HTML to preserve formatting. + parseResultTextView.text = if ("oppia-noninteractive-math" in newText) { + htmlParser.parseOppiaHtml(newText.replace("\n", "
"), parseResultTextView) + } else newText + } + + private fun computeParseResult(): String { + val expression = mathExpression.get() + val allowedVariables = allowedVariables.get() + ?.split(",") + ?.map { variable -> + machineLocale.run { + variable.toMachineLowerCase().trim() + } + } ?: listOf() + if (expression == null) { + return appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_parse_result_label, "Uninitialized" + ) + } + val parseResult = when (parseType) { + ParseType.NUMERIC_EXPRESSION -> { + MathExpressionParser.parseNumericExpression(expression) + .transformExpression(resultType, useDivAsFractions, mathExpressionAccessibilityUtil) + } + ParseType.ALGEBRAIC_EXPRESSION -> { + MathExpressionParser.parseAlgebraicExpression(expression, allowedVariables) + .transformExpression(resultType, useDivAsFractions, mathExpressionAccessibilityUtil) + } + ParseType.ALGEBRAIC_EQUATION -> { + MathExpressionParser.parseAlgebraicEquation(expression, allowedVariables) + .transformEquation(resultType, useDivAsFractions, mathExpressionAccessibilityUtil) + } + } + val parseResultStr = when (parseResult) { + is MathParsingResult.Failure -> parseResult.error.toString() + is MathParsingResult.Success -> parseResult.result + } + return appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_parse_result_label, "\n$parseResultStr" + ) + } + + /** Defines how text expressions should be parsed. */ + enum class ParseType { + /** Indicates that the user-inputted text should be parsed as a numeric expression. */ + NUMERIC_EXPRESSION, + + /** Indicates that the user-inputted text should be parsed as an algebraic expression. */ + ALGEBRAIC_EXPRESSION, + + /** Indicates that the user-inputted text should be parsed as an algebraic/math equation. */ + ALGEBRAIC_EQUATION + } + + /** Defines how the parsed expression/equation should be processed and displayed. */ + enum class ResultType { + /** Indicates that the raw parsed expression/equation proto should be displayed. */ + MATH_EXPRESSION, + + /** + * Indicates that the comparable operation representation proto of the expression/equation + * should be displayed. + */ + COMPARABLE_OPERATION, + + /** + * Indicates that the polynomial representation proto of the expression/equation should be + * displayed. + */ + POLYNOMIAL, + + /** Indicates that the expression should be converted to LaTeX and rendered as an image. */ + LATEX, + + /** + * Indicates that the expression should be converted to a human-readable accessibility string + * and displayed. + */ + HUMAN_READABLE_STRING + } + + private companion object { + private fun MathParsingResult.map(transform: (I) -> O): MathParsingResult { + return when (this) { + is MathParsingResult.Failure -> MathParsingResult.Failure(error) + is MathParsingResult.Success -> MathParsingResult.Success(transform(result)) + } + } + + private fun MathParsingResult.transformExpression( + resultType: ResultType, + useDivAsFractions: Boolean, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil + ): MathParsingResult { + return when (resultType) { + ResultType.MATH_EXPRESSION -> this + ResultType.COMPARABLE_OPERATION -> map { it.toComparableOperation() } + ResultType.POLYNOMIAL -> map { it.toPolynomial() } + ResultType.LATEX -> map { it.toRawLatex(useDivAsFractions).wrapAsLatexHtml() } + ResultType.HUMAN_READABLE_STRING -> map { + mathExpressionAccessibilityUtil.convertToHumanReadableString( + it, OppiaLanguage.ENGLISH, useDivAsFractions + ) + } + }.map { it.toString() } + } + + private fun MathParsingResult.transformEquation( + resultType: ResultType, + useDivAsFractions: Boolean, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil + ): MathParsingResult { + return when (resultType) { + ResultType.MATH_EXPRESSION -> this + ResultType.COMPARABLE_OPERATION -> map { + "Left side: ${it.leftSide.toComparableOperation()}" + + "\n\nRight side: ${it.rightSide.toComparableOperation()}" + } + ResultType.POLYNOMIAL -> map { + "Left side: ${it.leftSide.toPolynomial()}\n\nRight side: ${it.rightSide.toPolynomial()}" + } + ResultType.LATEX -> map { it.toRawLatex(useDivAsFractions).wrapAsLatexHtml() } + ResultType.HUMAN_READABLE_STRING -> map { + mathExpressionAccessibilityUtil.convertToHumanReadableString( + it, OppiaLanguage.ENGLISH, useDivAsFractions + ) + } + }.map { it.toString() } + } + + private fun String.wrapAsLatexHtml(): String { + val mathContentValue = + "{&quot;raw_latex&quot;:&quot;${this.replace("\\", "\\\\")}&quot;}" + return "" + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt index d367016774a..1caee17fa2d 100644 --- a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt @@ -164,14 +164,14 @@ class NavigationDrawerFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("NavigationDrawerFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun getCompletedStoryListCount(): LiveData { @@ -193,14 +193,18 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private fun processGetCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve completed story list", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "NavigationDrawerFragment", + "Failed to retrieve completed story list", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun getOngoingTopicListCount(): LiveData { @@ -222,14 +226,18 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private fun processGetOngoingTopicListResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve ongoing topic list", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "NavigationDrawerFragment", + "Failed to retrieve ongoing topic list", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } private fun openActivityByMenuItemId(menuItemId: Int) { diff --git a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt index 9813584970e..5ec6f40e761 100644 --- a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt +++ b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt @@ -14,6 +14,7 @@ import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeFragmen import org.oppia.android.app.devoptions.markchapterscompleted.MarkChaptersCompletedFragment import org.oppia.android.app.devoptions.markstoriescompleted.MarkStoriesCompletedFragment import org.oppia.android.app.devoptions.marktopicscompleted.MarkTopicsCompletedFragment +import org.oppia.android.app.devoptions.mathexpressionparser.MathExpressionParserFragment import org.oppia.android.app.devoptions.vieweventlogs.ViewEventLogsFragment import org.oppia.android.app.drawer.ExitProfileDialogFragment import org.oppia.android.app.drawer.NavigationDrawerFragment @@ -60,6 +61,7 @@ import org.oppia.android.app.settings.profile.ProfileResetPinFragment import org.oppia.android.app.shim.IntentFactoryShimModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.story.StoryFragment +import org.oppia.android.app.testing.ExplorationTestActivityPresenter import org.oppia.android.app.testing.ImageRegionSelectionTestFragment import org.oppia.android.app.topic.TopicFragment import org.oppia.android.app.topic.conceptcard.ConceptCardFragment @@ -111,6 +113,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(exitProfileDialogFragment: ExitProfileDialogFragment) fun inject(explorationFragment: ExplorationFragment) fun inject(explorationManagerFragment: ExplorationManagerFragment) + fun inject(explorationTestActivityTestFragment: ExplorationTestActivityPresenter.TestFragment) fun inject(faqListFragment: FAQListFragment) fun inject(forceNetworkTypeFragment: ForceNetworkTypeFragment) fun inject(helpFragment: HelpFragment) @@ -126,6 +129,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(markChapterCompletedFragment: MarkChaptersCompletedFragment) fun inject(markStoriesCompletedFragment: MarkStoriesCompletedFragment) fun inject(markTopicsCompletedFragment: MarkTopicsCompletedFragment) + fun inject(mathExpressionParserFragment: MathExpressionParserFragment) fun inject(myDownloadsFragment: MyDownloadsFragment) fun inject(navigationDrawerFragment: NavigationDrawerFragment) fun inject(onboardingFragment: OnboardingFragment) diff --git a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt index 24a7efb509f..16e3011628b 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt @@ -25,6 +25,7 @@ import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.toLiveData @@ -94,14 +95,18 @@ class HomeViewModel( */ val homeItemViewModelListLiveData: LiveData> by lazy { Transformations.map(homeItemViewModelListDataProvider.toLiveData()) { itemListResult -> - if (itemListResult.isFailure()) { - oppiaLogger.e( - "HomeFragment", - "No home fragment available -- failed to retrieve fragment data.", - itemListResult.getErrorOrNull() - ) + return@map when (itemListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HomeFragment", + "No home fragment available -- failed to retrieve fragment data.", + itemListResult.error + ) + listOf() + } + is AsyncResult.Pending -> listOf() + is AsyncResult.Success -> itemListResult.value } - return@map itemListResult.getOrDefault(listOf()) } } 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 c86e6e050bc..1a2647ff393 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 @@ -171,11 +171,12 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( } private fun getAssumedSuccessfulPromotedActivityList(): LiveData { - // If there's an error loading the data, assume the default. return Transformations.map(ongoingStoryListSummaryResultLiveData) { - it.getOrDefault( - PromotedActivityList.getDefaultInstance() - ) + when (it) { + // If there's an error loading the data, assume the default. + is AsyncResult.Failure, is AsyncResult.Pending -> PromotedActivityList.getDefaultInstance() + is AsyncResult.Success -> it.value + } } } @@ -252,7 +253,7 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) routeToResumeLessonListener.routeToResumeLesson( internalProfileId, @@ -260,9 +261,9 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( promotedStory.storyId, promotedStory.explorationId, backflowScreen = null, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) playExploration( promotedStory.topicId, @@ -298,17 +299,14 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("RecentlyPlayedFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "RecentlyPlayedFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("RecentlyPlayedFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("RecentlyPlayedFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("RecentlyPlayedFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt index bac0611a5b7..9d3e2fbd9fb 100644 --- a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt @@ -50,14 +50,18 @@ class OngoingTopicListViewModel @Inject constructor( private fun processOngoingTopicResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "OngoingTopicListFragment", - "Failed to retrieve OngoingTopicList: ", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "OngoingTopicListFragment", + "Failed to retrieve OngoingTopicList: ", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } private fun processOngoingTopicList( diff --git a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt index 4c27f79469d..acb9712e054 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt @@ -73,10 +73,14 @@ class OptionControlsViewModel @Inject constructor( } private fun processProfileResult(profile: AsyncResult): Profile { - if (profile.isFailure()) { - oppiaLogger.e("OptionsFragment", "Failed to retrieve profile", profile.getErrorOrNull()!!) + return when (profile) { + is AsyncResult.Failure -> { + oppiaLogger.e("OptionsFragment", "Failed to retrieve profile", profile.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profile.value } - return profile.getOrDefault(Profile.getDefaultInstance()) } private fun processProfileList(profile: Profile): List { diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt index e4b632d3d32..369f0efa623 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt @@ -21,6 +21,7 @@ import org.oppia.android.databinding.OptionStoryTextSizeBinding import org.oppia.android.databinding.OptionsFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import java.security.InvalidParameterException import javax.inject.Inject @@ -193,14 +194,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: small text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: small text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -212,14 +213,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: medium text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: medium text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -231,14 +232,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.LARGE_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: large text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.LARGE_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: large text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -251,14 +252,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.EXTRA_LARGE_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: extra large text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.EXTRA_LARGE_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: extra large text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -276,14 +277,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.ENGLISH_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: English", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.ENGLISH_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: English", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -295,14 +293,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.HINDI_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: Hindi", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.HINDI_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: Hindi", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -314,14 +309,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.CHINESE_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: Chinese", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.CHINESE_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: Chinese", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -333,14 +325,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.FRENCH_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: French", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.FRENCH_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: French", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -359,14 +348,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.NO_AUDIO - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: No Audio", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.NO_AUDIO + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: No Audio", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -378,14 +364,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: English", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: English", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -397,14 +380,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: Hindi", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Hindi", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -416,14 +396,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: Chinese", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Chinese", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -435,14 +412,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: French", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: French", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) diff --git a/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt new file mode 100644 index 00000000000..731d26e0590 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt @@ -0,0 +1,45 @@ +package org.oppia.android.app.parser + +import androidx.annotation.StringRes +import org.oppia.android.R +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.math.FractionParser.FractionParsingError + +/** Enum to store the errors of [FractionInputInteractionView]. */ +enum class FractionParsingUiError(@StringRes private var error: Int?) { + /** Corresponds to [FractionParsingError.VALID]. */ + VALID(error = null), + + /** Corresponds to [FractionParsingError.INVALID_CHARS]. */ + INVALID_CHARS(error = R.string.fraction_error_invalid_chars), + + /** Corresponds to [FractionParsingError.INVALID_FORMAT]. */ + INVALID_FORMAT(error = R.string.fraction_error_invalid_format), + + /** Corresponds to [FractionParsingError.DIVISION_BY_ZERO]. */ + DIVISION_BY_ZERO(error = R.string.fraction_error_divide_by_zero), + + /** Corresponds to [FractionParsingError.NUMBER_TOO_LONG]. */ + NUMBER_TOO_LONG(error = R.string.fraction_error_larger_than_seven_digits); + + /** + * Returns the string corresponding to this error's string resources, or null if there is none. + */ + fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? = + error?.let(resourceHandler::getStringInLocale) + + companion object { + /** + * Returns the [FractionParsingUiError] corresponding to the specified [FractionParsingError]. + */ + fun createFromParsingError(parsingError: FractionParsingError): FractionParsingUiError { + return when (parsingError) { + FractionParsingError.VALID -> VALID + FractionParsingError.INVALID_CHARS -> INVALID_CHARS + FractionParsingError.INVALID_FORMAT -> INVALID_FORMAT + FractionParsingError.DIVISION_BY_ZERO -> DIVISION_BY_ZERO + FractionParsingError.NUMBER_TOO_LONG -> NUMBER_TOO_LONG + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt index 3670e2fd16c..5b625623ad2 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt @@ -3,7 +3,7 @@ package org.oppia.android.app.parser import androidx.annotation.StringRes import org.oppia.android.R import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace /** * This class contains methods that help to parse string to number, check realtime and submit time diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt index 1d2562fa73a..31895402263 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt @@ -4,8 +4,8 @@ import androidx.annotation.StringRes import org.oppia.android.R import org.oppia.android.app.model.RatioExpression import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace -import org.oppia.android.domain.util.removeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace +import org.oppia.android.util.extensions.removeWhitespace /** * Utility for parsing [RatioExpression]s from strings and validating strings can be parsed diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt index d552408ca6e..14c5478b70f 100755 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt @@ -71,10 +71,9 @@ class AudioFragmentPresenter @Inject constructor( .observe( fragment, Observer> { - if (it.isSuccess()) { - val prefs = it.getOrDefault(CellularDataPreference.getDefaultInstance()) - showCellularDataDialog = !(prefs.hideDialog) - useCellularData = prefs.useCellularData + if (it is AsyncResult.Success) { + showCellularDataDialog = !it.value.hideDialog + useCellularData = it.value.useCellularData } } ) @@ -143,10 +142,15 @@ class AudioFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): String { - if (profileResult.isFailure()) { - oppiaLogger.e("AudioFragment", "Failed to retrieve profile", profileResult.getErrorOrNull()!!) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("AudioFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return getAudioLanguage(profileResult.getOrDefault(Profile.getDefaultInstance()).audioLanguage) + return getAudioLanguage(profile.audioLanguage) } /** Sets selected language code in presenter and ViewModel */ diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt index d18048e74f1..5c1981790e5 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt @@ -142,26 +142,26 @@ class AudioViewModel @Inject constructor( } private fun processDurationResultLiveData(playProgressResult: AsyncResult): Int { - if (!playProgressResult.isSuccess()) { + if (playProgressResult !is AsyncResult.Success) { return 0 } - return playProgressResult.getOrThrow().duration + return playProgressResult.value.duration } private fun processPositionResultLiveData(playProgressResult: AsyncResult): Int { - if (!playProgressResult.isSuccess()) { + if (playProgressResult !is AsyncResult.Success) { return 0 } - return playProgressResult.getOrThrow().position + return playProgressResult.value.position } private fun processPlayStatusResultLiveData( playProgressResult: AsyncResult ): UiAudioPlayStatus { - return when { - playProgressResult.isPending() -> UiAudioPlayStatus.LOADING - playProgressResult.isFailure() -> UiAudioPlayStatus.FAILED - else -> when (playProgressResult.getOrThrow().type) { + return when (playProgressResult) { + is AsyncResult.Pending -> UiAudioPlayStatus.LOADING + is AsyncResult.Failure -> UiAudioPlayStatus.FAILED + is AsyncResult.Success -> when (playProgressResult.value.type) { PlayStatus.PREPARED -> { if (autoPlay) audioPlayerController.play() autoPlay = false diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index aa1edfe9e04..4f131159d98 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -238,18 +238,15 @@ class ExplorationActivityPresenter @Inject constructor( fun stopExploration() { fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE.name) - explorationDataController.stopPlayingExploration() + explorationDataController.stopPlayingExploration().toLiveData() .observe( activity, Observer> { - when { - it.isPending() -> oppiaLogger.d("ExplorationActivity", "Stopping exploration") - it.isFailure() -> oppiaLogger.e( - "ExplorationActivity", - "Failed to stop exploration", - it.getErrorOrNull()!! - ) - else -> { + when (it) { + is AsyncResult.Pending -> oppiaLogger.d("ExplorationActivity", "Stopping exploration") + is AsyncResult.Failure -> + oppiaLogger.e("ExplorationActivity", "Failed to stop exploration", it.error) + is AsyncResult.Success -> { oppiaLogger.d("ExplorationActivity", "Successfully stopped exploration") backPressActivitySelector(backflowScreen) (activity as ExplorationActivity).finish() @@ -320,14 +317,16 @@ class ExplorationActivityPresenter @Inject constructor( /** Helper for subscribeToExploration. */ private fun processExploration(ephemeralStateResult: AsyncResult): Exploration { - if (ephemeralStateResult.isFailure()) { - oppiaLogger.e( - "ExplorationActivity", - "Failed to retrieve answer outcome", - ephemeralStateResult.getErrorOrNull()!! - ) + return when (ephemeralStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationActivity", "Failed to retrieve answer outcome", ephemeralStateResult.error + ) + Exploration.getDefaultInstance() + } + is AsyncResult.Pending -> Exploration.getDefaultInstance() + is AsyncResult.Success -> ephemeralStateResult.value } - return ephemeralStateResult.getOrDefault(Exploration.getDefaultInstance()) } private fun backPressActivitySelector(backflowScreen: Int?) { @@ -420,15 +419,17 @@ class ExplorationActivityPresenter @Inject constructor( ).toLiveData().observe( activity, Observer { - if (it.isSuccess()) { - oldestCheckpointExplorationId = it.getOrThrow().explorationId - oldestCheckpointExplorationTitle = it.getOrThrow().explorationTitle - } else if (it.isFailure()) { - oppiaLogger.e( - "ExplorationActivity", - "Failed to retrieve oldest saved checkpoint details.", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> { + oldestCheckpointExplorationId = it.value.explorationId + oldestCheckpointExplorationTitle = it.value.explorationTitle + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationActivity", "Failed to retrieve oldest saved checkpoint details.", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for an actual result. } } ) diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt index 28dc4156aac..4df78475693 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt @@ -45,13 +45,15 @@ class ExplorationManagerFragmentPresenter @Inject constructor( private fun processReadingTextSizeResult( readingTextSizeResult: AsyncResult ): ReadingTextSize { - if (readingTextSizeResult.isFailure()) { - oppiaLogger.e( - "ExplorationManagerFragment", - "Failed to retrieve profile", - readingTextSizeResult.getErrorOrNull()!! - ) - } - return readingTextSizeResult.getOrDefault(Profile.getDefaultInstance()).readingTextSize + return when (readingTextSizeResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationManagerFragment", "Failed to retrieve profile", readingTextSizeResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> readingTextSizeResult.value + }.readingTextSize } } 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 a8460e014ff..2f7e4613911 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 @@ -40,25 +40,25 @@ class HintsAndSolutionExplorationManagerFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "HintsAndSolutionExplorationManagerFragmentPresenter", - "Failed to retrieve ephemeral state", - result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { + when (result) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HintsAndSolutionExplorationManagerFragmentPresenter", + "Failed to retrieve ephemeral state", + result.error + ) + } // Display nothing until a valid result is available. - return - } - - val ephemeralState = result.getOrThrow() - - // Check if hints are available for this state. - if (ephemeralState.state.interaction.hintList.size != 0) { - (activity as HintsAndSolutionExplorationManagerListener).onExplorationStateLoaded( - ephemeralState.state, ephemeralState.writtenTranslationContext - ) + is AsyncResult.Pending -> {} + is AsyncResult.Success -> { + // Check if hints are available for this state. + val ephemeralState = result.value + if (ephemeralState.state.interaction.hintList.size != 0) { + (activity as HintsAndSolutionExplorationManagerListener).onExplorationStateLoaded( + ephemeralState.state, ephemeralState.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 4af453b01d8..e3eafd4baee 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 @@ -39,6 +39,7 @@ import org.oppia.android.domain.exploration.ExplorationProgressController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.StoryProgressController import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType @@ -282,19 +283,15 @@ class StateFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", - "Failed to retrieve ephemeral state", - result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { - // Display nothing until a valid result is available. - return + when (result) { + is AsyncResult.Failure -> + oppiaLogger.e("StateFragment", "Failed to retrieve ephemeral state", result.error) + is AsyncResult.Pending -> {} // Display nothing until a valid result is available. + is AsyncResult.Success -> processEphemeralState(result.value) } + } - val ephemeralState = result.getOrThrow() + private fun processEphemeralState(ephemeralState: EphemeralState) { explorationCheckpointState = ephemeralState.checkpointState val shouldSplit = splitScreenManager.shouldSplitScreen(ephemeralState.state.interaction.id) if (shouldSplit) { @@ -333,14 +330,12 @@ class StateFragmentPresenter @Inject constructor( } /** Subscribes to the result of requesting to show a hint or solution. */ - private fun subscribeToHintSolution(resultLiveData: LiveData>) { - resultLiveData.observe( + private fun subscribeToHintSolution(resultDataProvider: DataProvider) { + resultDataProvider.toLiveData().observe( fragment, { result -> - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", "Failed to retrieve hint/solution", result.getErrorOrNull()!! - ) + if (result is AsyncResult.Failure) { + oppiaLogger.e("StateFragment", "Failed to retrieve hint/solution", result.error) } else { // If the hint/solution, was revealed remove dot and radar. viewModel.setHintOpenedAndUnRevealedVisibility(false) @@ -389,18 +384,20 @@ class StateFragmentPresenter @Inject constructor( private fun processAnswerOutcome( ephemeralStateResult: AsyncResult ): AnswerOutcome { - if (ephemeralStateResult.isFailure()) { - oppiaLogger.e( - "StateFragment", - "Failed to retrieve answer outcome", - ephemeralStateResult.getErrorOrNull()!! - ) + return when (ephemeralStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "StateFragment", "Failed to retrieve answer outcome", ephemeralStateResult.error + ) + AnswerOutcome.getDefaultInstance() + } + is AsyncResult.Pending -> AnswerOutcome.getDefaultInstance() + is AsyncResult.Success -> ephemeralStateResult.value } - return ephemeralStateResult.getOrDefault(AnswerOutcome.getDefaultInstance()) } private fun handleSubmitAnswer(answer: UserAnswer) { - subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer)) + subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer).toLiveData()) } fun dismissConceptCard() { @@ -413,7 +410,7 @@ class StateFragmentPresenter @Inject constructor( private fun moveToNextState() { viewModel.setCanSubmitAnswer(canSubmitAnswer = false) - explorationProgressController.moveToNextState().observe( + explorationProgressController.moveToNextState().toLiveData().observe( fragment, Observer { recyclerViewAssembler.collapsePreviousResponses() 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 8375b12f475..99954487d9b 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 @@ -41,7 +41,7 @@ import org.oppia.android.app.player.state.itemviewmodel.DragAndDropSortInteracti import org.oppia.android.app.player.state.itemviewmodel.FeedbackViewModel import org.oppia.android.app.player.state.itemviewmodel.FractionInteractionViewModel import org.oppia.android.app.player.state.itemviewmodel.ImageRegionSelectionInteractionViewModel -import org.oppia.android.app.player.state.itemviewmodel.InteractionViewModelFactory +import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel import org.oppia.android.app.player.state.itemviewmodel.NextButtonViewModel import org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel import org.oppia.android.app.player.state.itemviewmodel.PreviousButtonViewModel @@ -51,6 +51,7 @@ import org.oppia.android.app.player.state.itemviewmodel.ReplayButtonViewModel import org.oppia.android.app.player.state.itemviewmodel.ReturnToTopicButtonViewModel import org.oppia.android.app.player.state.itemviewmodel.SelectionInteractionViewModel import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel +import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.InteractionItemFactory import org.oppia.android.app.player.state.itemviewmodel.SubmitButtonViewModel import org.oppia.android.app.player.state.itemviewmodel.SubmittedAnswerViewModel import org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel @@ -74,6 +75,7 @@ import org.oppia.android.databinding.DragDropInteractionItemBinding import org.oppia.android.databinding.FeedbackItemBinding import org.oppia.android.databinding.FractionInteractionItemBinding import org.oppia.android.databinding.ImageRegionSelectionInteractionItemBinding +import org.oppia.android.databinding.MathExpressionInteractionsItemBinding import org.oppia.android.databinding.NextButtonItemBinding import org.oppia.android.databinding.NumericInputInteractionItemBinding import org.oppia.android.databinding.PreviousButtonItemBinding @@ -138,8 +140,7 @@ class StatePlayerRecyclerViewAssembler private constructor( private val currentStateName: ObservableField?, private val isAudioPlaybackEnabled: ObservableField?, private val audioUiManagerRetriever: AudioUiManagerRetriever?, - private val interactionViewModelFactoryMap: Map< - String, @JvmSuppressWildcards InteractionViewModelFactory>, + private val interactionViewModelFactoryMap: Map, backgroundCoroutineDispatcher: CoroutineDispatcher, private val hasConversationView: Boolean, private val resourceHandler: AppLanguageResourceHandler, @@ -159,8 +160,6 @@ class StatePlayerRecyclerViewAssembler private constructor( */ private var hasPreviousResponsesExpanded: Boolean = false - val isCorrectAnswer = ObservableField(false) - private val lifecycleSafeTimerFactory = LifecycleSafeTimerFactory(backgroundCoroutineDispatcher) /** The most recent content ID read by the audio system. */ @@ -220,7 +219,7 @@ class StatePlayerRecyclerViewAssembler private constructor( conversationPendingItemList, extraInteractionPendingItemList, ephemeralState.pendingState.wrongAnswerList, - /* isCorrectAnswer= */ false, + isLastAnswerCorrect = false, gcsEntityId, ephemeralState.writtenTranslationContext ) @@ -246,12 +245,11 @@ class StatePlayerRecyclerViewAssembler private constructor( // Ensure the answer is marked in situations where that's guaranteed (e.g. completed state) // so that the UI always has the correct answer indication, even after configuration changes. - isCorrectAnswer.set(true) addPreviousAnswers( conversationPendingItemList, extraInteractionPendingItemList, ephemeralState.completedState.answerList, - /* isCorrectAnswer= */ true, + isLastAnswerCorrect = true, gcsEntityId, ephemeralState.writtenTranslationContext ) @@ -310,7 +308,7 @@ class StatePlayerRecyclerViewAssembler private constructor( writtenTranslationContext: WrittenTranslationContext ) { val interactionViewModelFactory = interactionViewModelFactoryMap.getValue(interaction.id) - pendingItemList += interactionViewModelFactory( + pendingItemList += interactionViewModelFactory.create( gcsEntityId, hasConversationView, interaction, @@ -346,7 +344,7 @@ class StatePlayerRecyclerViewAssembler private constructor( pendingItemList: MutableList, rightPendingItemList: MutableList, answersAndResponses: List, - isCorrectAnswer: Boolean, + isLastAnswerCorrect: Boolean, gcsEntityId: String, writtenTranslationContext: WrittenTranslationContext ) { @@ -369,6 +367,8 @@ class StatePlayerRecyclerViewAssembler private constructor( hasPreviousResponsesExpanded for (answerAndResponse in answersAndResponses.take(answersAndResponses.size - 1)) { if (playerFeatureSet.pastAnswerSupport) { + // Earlier answers can't be correct (since otherwise new answers wouldn't be able to be + // submitted), hence the assumption that these aren't. createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, @@ -396,17 +396,17 @@ class StatePlayerRecyclerViewAssembler private constructor( } answersAndResponses.lastOrNull()?.let { answerAndResponse -> if (playerFeatureSet.pastAnswerSupport) { - if (isCorrectAnswer && isSplitView.get()!!) { + if (isLastAnswerCorrect && isSplitView.get()!!) { rightPendingItemList += createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, - /* isAnswerCorrect= */ true + isAnswerCorrect = true ) } else { pendingItemList += createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, - this.isCorrectAnswer.get()!! + isLastAnswerCorrect || answerAndResponse.isCorrectAnswer ) } } @@ -883,7 +883,7 @@ class StatePlayerRecyclerViewAssembler private constructor( private val fragment: Fragment, private val profileId: ProfileId, private val context: Context, - private val interactionViewModelFactoryMap: Map, + private val interactionViewModelFactoryMap: Map, private val backgroundCoroutineDispatcher: CoroutineDispatcher, private val resourceHandler: AppLanguageResourceHandler, private val translationController: TranslationController @@ -1027,6 +1027,21 @@ class StatePlayerRecyclerViewAssembler private constructor( inflateDataBinding = RatioInputInteractionItemBinding::inflate, setViewModel = RatioInputInteractionItemBinding::setViewModel, transformViewModel = { it as RatioExpressionInputInteractionViewModel } + ).registerViewDataBinder( + viewType = StateItemViewModel.ViewType.NUMERIC_EXPRESSION_INPUT_INTERACTION, + inflateDataBinding = MathExpressionInteractionsItemBinding::inflate, + setViewModel = MathExpressionInteractionsItemBinding::setViewModel, + transformViewModel = { it as MathExpressionInteractionsViewModel } + ).registerViewDataBinder( + viewType = StateItemViewModel.ViewType.ALGEBRAIC_EXPRESSION_INPUT_INTERACTION, + inflateDataBinding = MathExpressionInteractionsItemBinding::inflate, + setViewModel = MathExpressionInteractionsItemBinding::setViewModel, + transformViewModel = { it as MathExpressionInteractionsViewModel } + ).registerViewDataBinder( + viewType = StateItemViewModel.ViewType.MATH_EQUATION_INPUT_INTERACTION, + inflateDataBinding = MathExpressionInteractionsItemBinding::inflate, + setViewModel = MathExpressionInteractionsItemBinding::setViewModel, + transformViewModel = { it as MathExpressionInteractionsViewModel } ).registerViewDataBinder( viewType = StateItemViewModel.ViewType.SUBMIT_ANSWER_BUTTON, inflateDataBinding = SubmitButtonItemBinding::inflate, @@ -1055,6 +1070,9 @@ class StatePlayerRecyclerViewAssembler private constructor( when (userAnswer.textualAnswerCase) { UserAnswer.TextualAnswerCase.HTML_ANSWER -> { showSingleAnswer(binding) + val accessibleAnswer = if (userAnswer.contentDescription.isNotEmpty()) { + userAnswer.contentDescription + } else null val htmlParser = htmlParserFactory.create( resourceBucketName, entityType, @@ -1067,7 +1085,8 @@ class StatePlayerRecyclerViewAssembler private constructor( userAnswer.htmlAnswer, binding.submittedAnswerTextView, supportsConceptCards = submittedAnswerViewModel.supportsConceptCards - ) + ), + accessibleAnswer ) } UserAnswer.TextualAnswerCase.LIST_OF_HTML_ANSWERS -> { @@ -1366,7 +1385,7 @@ class StatePlayerRecyclerViewAssembler private constructor( private val fragment: Fragment, private val context: Context, private val interactionViewModelFactoryMap: Map< - String, @JvmSuppressWildcards InteractionViewModelFactory>, + String, @JvmSuppressWildcards InteractionItemFactory>, @BackgroundDispatcher private val backgroundCoroutineDispatcher: CoroutineDispatcher, private val resourceHandler: AppLanguageResourceHandler, private val translationController: 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 dbabbbd32ee..e8c2a70786a 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 @@ -1,11 +1,15 @@ package org.oppia.android.app.player.state.itemviewmodel +import androidx.fragment.app.Fragment +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.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.android.app.player.state.listener.PreviousNavigationButtonListener +import javax.inject.Inject // For context: // https://github.com/oppia/oppia/blob/37285a/extensions/interactions/Continue/directives/oppia-interactive-continue.directive.ts @@ -17,7 +21,7 @@ private const val DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER = "Please continue." * from [ContinueNavigationButtonViewModel] in that the latter is for an already completed state, whereas this * represents an actual interaction. */ -class ContinueInteractionViewModel( +class ContinueInteractionViewModel private constructor( private val interactionAnswerReceiver: InteractionAnswerReceiver, val hasConversationView: Boolean, val hasPreviousButton: Boolean, @@ -41,4 +45,29 @@ class ContinueInteractionViewModel( fun handleButtonClicked() { interactionAnswerReceiver.onAnswerReadyForSubmission(getPendingAnswer()) } + + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val fragment: Fragment + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return ContinueInteractionViewModel( + interactionAnswerReceiver, + hasConversationView, + hasPreviousButton, + fragment as PreviousNavigationButtonListener, + isSplitView, + writtenTranslationContext + ) + } + } } 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 6eb8dee40bc..1509d9a295b 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 @@ -15,14 +15,16 @@ 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.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 +import javax.inject.Inject /** [StateItemViewModel] for drag drop & sort choice list. */ -class DragAndDropSortInteractionViewModel( +class DragAndDropSortInteractionViewModel private constructor( val entityId: String, val hasConversationView: Boolean, interaction: Interaction, @@ -188,6 +190,34 @@ class DragAndDropSortInteractionViewModel( (adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems) } + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return DragAndDropSortInteractionViewModel( + entityId, + hasConversationView, + interaction, + answerErrorReceiver, + isSplitView, + writtenTranslationContext, + resourceHandler, + translationController + ) + } + } + companion object { private fun computeChoiceItems( contentIdHtmlMap: Map, 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 b2e2329cefd..54ee1566a38 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 @@ -9,15 +9,18 @@ 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.parser.FractionParsingUiError import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.math.FractionParser +import javax.inject.Inject /** [StateItemViewModel] for the fraction input interaction. */ -class FractionInteractionViewModel( +class FractionInteractionViewModel private constructor( interaction: Interaction, val hasConversationView: Boolean, val isSplitView: Boolean, @@ -32,7 +35,7 @@ class FractionInteractionViewModel( var errorMessage = ObservableField("") val hintText: CharSequence = deriveHintText(interaction) - private val stringToFractionParser: StringToFractionParser = StringToFractionParser() + private val fractionParser = FractionParser() init { val callback: Observable.OnPropertyChangedCallback = @@ -52,7 +55,7 @@ class FractionInteractionViewModel( if (answerText.isNotEmpty()) { val answerTextString = answerText.toString() answer = InteractionObject.newBuilder().apply { - fraction = stringToFractionParser.parseFractionFromString(answerTextString) + fraction = fractionParser.parseFractionFromString(answerTextString) }.build() plainAnswer = answerTextString this.writtenTranslationContext = this@FractionInteractionViewModel.writtenTranslationContext @@ -63,14 +66,18 @@ class FractionInteractionViewModel( override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { if (answerText.isNotEmpty()) { when (category) { - AnswerErrorCategory.REAL_TIME -> + AnswerErrorCategory.REAL_TIME -> { pendingAnswerError = - stringToFractionParser.getRealTimeAnswerError(answerText.toString()) - .getErrorMessageFromStringRes(resourceHandler) - AnswerErrorCategory.SUBMIT_TIME -> + FractionParsingUiError.createFromParsingError( + fractionParser.getRealTimeAnswerError(answerText.toString()) + ).getErrorMessageFromStringRes(resourceHandler) + } + AnswerErrorCategory.SUBMIT_TIME -> { pendingAnswerError = - stringToFractionParser.getSubmitTimeError(answerText.toString()) - .getErrorMessageFromStringRes(resourceHandler) + FractionParsingUiError.createFromParsingError( + fractionParser.getSubmitTimeError(answerText.toString()) + ).getErrorMessageFromStringRes(resourceHandler) + } } errorMessage.set(pendingAnswerError) } @@ -120,4 +127,31 @@ class FractionInteractionViewModel( else -> resourceHandler.getStringInLocale(R.string.fractions_default_hint_text) } } + + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return FractionInteractionViewModel( + interaction, + hasConversationView, + isSplitView, + answerErrorReceiver, + writtenTranslationContext, + resourceHandler, + translationController + ) + } + } } 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 7079d3b10b6..47b6a5e4569 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 @@ -11,14 +11,16 @@ 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.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.DefaultRegionClickedEvent import org.oppia.android.app.utility.NamedRegionClickedEvent import org.oppia.android.app.utility.OnClickableAreaClickedListener import org.oppia.android.app.utility.RegionClickedEvent +import javax.inject.Inject /** [StateItemViewModel] for image region selection. */ -class ImageRegionSelectionInteractionViewModel( +class ImageRegionSelectionInteractionViewModel private constructor( val entityId: String, val hasConversationView: Boolean, interaction: Interaction, @@ -88,4 +90,30 @@ class ImageRegionSelectionInteractionViewModel( .addClickedRegions(region?.label ?: "") .build() } + + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return ImageRegionSelectionInteractionViewModel( + entityId, + hasConversationView, + interaction, + answerErrorReceiver, + isSplitView, + writtenTranslationContext, + resourceHandler + ) + } + } } 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 deleted file mode 100644 index 2ad060e78e3..00000000000 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelFactory.kt +++ /dev/null @@ -1,22 +0,0 @@ -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 - -/** - * Returns a new [StateItemViewModel] corresponding to this interaction with the GCS entity ID, the [Interaction] - * object corresponding to the interaction view, a receiver for answers if this interaction pushes answers, and whether - * there's a previous button enabled (only relevant for navigation-based interactions). - */ -typealias InteractionViewModelFactory = ( - entityId: String, - hasConversationView: Boolean, - interaction: Interaction, - interactionAnswerReceiver: InteractionAnswerReceiver, - interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length - hasPreviousButton: 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 5ad96318870..51ea00e2e6e 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 @@ -1,191 +1,114 @@ package org.oppia.android.app.player.state.itemviewmodel -import androidx.fragment.app.Fragment +import dagger.Binds import dagger.Module import dagger.Provides 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 * explicitly displayed to the user. */ @Module -class InteractionViewModelModule { - companion object { - val splitScreenInteractionIdsPool = listOf("DragAndDropSortInput", "ImageClickInput") - } - +interface InteractionViewModelModule { // TODO(#300): Use a common source for these interaction IDs to de-duplicate them from // other places in the codebase where they are referenced. - @Provides + @Binds @IntoMap @StringKey("Continue") - fun provideContinueInteractionViewModelFactory(fragment: Fragment): InteractionViewModelFactory { - return { _, hasConversationView, _, interactionAnswerReceiver, _, hasPreviousButton, - isSplitView, writtenTranslationContext -> - ContinueInteractionViewModel( - interactionAnswerReceiver, - hasConversationView, - hasPreviousButton, - fragment as PreviousNavigationButtonListener, - isSplitView, - writtenTranslationContext - ) - } - } + fun provideContinueInteractionViewModelFactory( + factoryImpl: ContinueInteractionViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("MultipleChoiceInput") fun provideMultipleChoiceInputViewModelFactory( - translationController: TranslationController - ): InteractionViewModelFactory { - return { entityId, hasConversationView, interaction, _, - interactionAnswerErrorReceiver, _, isSplitView, writtenTranslationContext -> - SelectionInteractionViewModel( - entityId, - hasConversationView, - interaction, - interactionAnswerErrorReceiver, - isSplitView, - writtenTranslationContext, - translationController - ) - } - } + factoryImpl: SelectionInteractionViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("ItemSelectionInput") fun provideItemSelectionInputViewModelFactory( - translationController: TranslationController - ): InteractionViewModelFactory { - return { entityId, hasConversationView, interaction, _, - interactionAnswerErrorReceiver, _, isSplitView, writtenTranslationContext -> - SelectionInteractionViewModel( - entityId, - hasConversationView, - interaction, - interactionAnswerErrorReceiver, - isSplitView, - writtenTranslationContext, - translationController - ) - } - } + factoryImpl: SelectionInteractionViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("FractionInput") fun provideFractionInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler, - translationController: TranslationController - ): InteractionViewModelFactory { - return { _, hasConversationView, interaction, _, interactionAnswerErrorReceiver, _, - isSplitView, writtenTranslationContext -> - FractionInteractionViewModel( - interaction, - hasConversationView, - isSplitView, - interactionAnswerErrorReceiver, - writtenTranslationContext, - resourceHandler, - translationController - ) - } - } + factoryImpl: FractionInteractionViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("NumericInput") fun provideNumericInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler - ): InteractionViewModelFactory { - return { _, hasConversationView, _, _, interactionAnswerErrorReceiver, _, isSplitView, - writtenTranslationContext -> - NumericInputViewModel( - hasConversationView, - interactionAnswerErrorReceiver, - isSplitView, - writtenTranslationContext, - resourceHandler - ) - } - } + factoryImpl: NumericInputViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("TextInput") fun provideTextInputViewModelFactory( - translationController: TranslationController - ): InteractionViewModelFactory { - return { _, hasConversationView, interaction, _, interactionAnswerErrorReceiver, _, - isSplitView, writtenTranslationContext -> - TextInputViewModel( - interaction, hasConversationView, interactionAnswerErrorReceiver, isSplitView, - writtenTranslationContext, translationController - ) - } - } + factoryImpl: TextInputViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("DragAndDropSortInput") fun provideDragAndDropSortInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler, - translationController: TranslationController - ): InteractionViewModelFactory { - return { entityId, hasConversationView, interaction, _, interactionAnswerErrorReceiver, _, - isSplitView, writtenTranslationContext -> - DragAndDropSortInteractionViewModel( - entityId, hasConversationView, interaction, interactionAnswerErrorReceiver, isSplitView, - writtenTranslationContext, resourceHandler, translationController - ) - } - } + factoryImpl: DragAndDropSortInteractionViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("ImageClickInput") fun provideImageClickInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler - ): InteractionViewModelFactory { - return { entityId, hasConversationView, interaction, _, answerErrorReceiver, _, isSplitView, - writtenTranslationContext -> - ImageRegionSelectionInteractionViewModel( - entityId, - hasConversationView, - interaction, - answerErrorReceiver, - isSplitView, - writtenTranslationContext, - resourceHandler - ) - } - } + factoryImpl: ImageRegionSelectionInteractionViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory - @Provides + @Binds @IntoMap @StringKey("RatioExpressionInput") fun provideRatioExpressionInputViewModelFactory( - resourceHandler: AppLanguageResourceHandler, - translationController: TranslationController - ): InteractionViewModelFactory { - return { _, hasConversationView, interaction, _, answerErrorReceiver, _, isSplitView, - writtenTranslationContext -> - RatioExpressionInputInteractionViewModel( - interaction, - hasConversationView, - isSplitView, - answerErrorReceiver, - writtenTranslationContext, - resourceHandler, - translationController - ) + factoryImpl: RatioExpressionInputInteractionViewModel.FactoryImpl + ): StateItemViewModel.InteractionItemFactory + + // Note that Dagger doesn't support mixing binds & provides methods. See + // https://stackoverflow.com/a/54592300 for the origin of this approach. + @Module + companion object { + @Provides + @IntoMap + @StringKey("NumericExpressionInput") + @JvmStatic + fun provideNumericExpressionInputViewModelFactory( + factoryFactoryImpl: MathExpressionInteractionsViewModel.FactoryImpl.FactoryFactoryImpl + ): StateItemViewModel.InteractionItemFactory { + return factoryFactoryImpl.createFactoryForNumericExpression() + } + + @Provides + @IntoMap + @StringKey("AlgebraicExpressionInput") + @JvmStatic + fun provideAlgebraicExpressionInputViewModelFactory( + factoryFactoryImpl: MathExpressionInteractionsViewModel.FactoryImpl.FactoryFactoryImpl + ): StateItemViewModel.InteractionItemFactory { + return factoryFactoryImpl.createFactoryForAlgebraicExpression() + } + + @Provides + @IntoMap + @StringKey("MathEquationInput") + @JvmStatic + fun provideMathEquationInputViewModelFactory( + factoryFactoryImpl: MathExpressionInteractionsViewModel.FactoryImpl.FactoryFactoryImpl + ): StateItemViewModel.InteractionItemFactory { + return factoryFactoryImpl.createFactoryForMathEquation() } } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt new file mode 100644 index 00000000000..77687c70a5d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt @@ -0,0 +1,648 @@ +package org.oppia.android.app.player.state.itemviewmodel + +import android.text.Editable +import android.text.TextWatcher +import androidx.annotation.StringRes +import androidx.databinding.Observable +import androidx.databinding.ObservableField +import org.oppia.android.R +import org.oppia.android.app.model.Interaction +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory +import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver +import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver +import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel.FactoryImpl.FactoryFactoryImpl +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil +import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.oppia.android.util.math.toPlainText +import org.oppia.android.util.math.toRawLatex +import javax.inject.Inject +import org.oppia.android.app.model.MathBinaryOperation.Operator as UnaryOperator + +/** + * [StateItemViewModel] for input for numeric expressions, algebraic expressions, and math + * (algebraic) equations. + */ +class MathExpressionInteractionsViewModel private constructor( + interaction: Interaction, + val hasConversationView: Boolean, + private val errorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + private val writtenTranslationContext: WrittenTranslationContext, + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController, + private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + private val interactionType: InteractionType +) : StateItemViewModel(interactionType.viewType), InteractionAnswerHandler { + private var pendingAnswerError: String? = null + + /** + * Defines the current answer text being entered by the learner. This is expected to be directly + * bound to the corresponding edit text. + */ + var answerText: CharSequence = "" + + /** + * Defines whether an answer is currently available to parse. This is expected to be directly + * bound to the UI. + */ + var isAnswerAvailable = ObservableField(false) + + /** + * Specifies the current error caused by the current answer (if any; this is empty if there is no + * error). This is expected to be directly bound to the UI. + */ + var errorMessage = ObservableField("") + + /** Specifies the text to show in the answer box when no text is entered. */ + val hintText: CharSequence = deriveHintText(interaction) + + private val allowedVariables = retrieveAllowedVariables(interaction) + private val useFractionsForDivision = + interaction.customizationArgsMap["useFractionForDivision"]?.boolValue ?: false + + init { + val callback: Observable.OnPropertyChangedCallback = + object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable, propertyId: Int) { + errorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck( + pendingAnswerError, + answerText.isNotEmpty() + ) + } + } + errorMessage.addOnPropertyChangedCallback(callback) + isAnswerAvailable.addOnPropertyChangedCallback(callback) + } + + override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { + if (answerText.isNotEmpty()) { + val answerTextString = answerText.toString() + answer = InteractionObject.newBuilder().apply { + mathExpression = answerTextString + }.build() + + // Since the LaTeX is embedded without a JSON object, backslashes need to be double escaped. + val answerAsLatex = + interactionType.computeLatex( + answerTextString, useFractionsForDivision, allowedVariables + )?.replace("\\", "\\\\") + if (answerAsLatex != null) { + val mathContentValue = "{&quot;raw_latex&quot;:&quot;$answerAsLatex&quot;}" + htmlAnswer = + "" + } else plainAnswer = answerTextString + + contentDescription = + interactionType.computeHumanReadableString( + answerTextString, + useFractionsForDivision, + allowedVariables, + mathExpressionAccessibilityUtil, + this@MathExpressionInteractionsViewModel.writtenTranslationContext.language + ) ?: answerTextString + + this.writtenTranslationContext = + this@MathExpressionInteractionsViewModel.writtenTranslationContext + } + }.build() + + override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + if (answerText.isNotEmpty()) { + pendingAnswerError = when (category) { + // There's no support for real-time errors. + AnswerErrorCategory.REAL_TIME -> null + AnswerErrorCategory.SUBMIT_TIME -> { + interactionType.computeSubmitTimeError( + answerText.toString(), allowedVariables, resourceHandler + ) + } + } + errorMessage.set(pendingAnswerError) + } + return pendingAnswerError + } + + /** + * Returns the [TextWatcher] which helps track the current pending answer and whether there is one + * presently being entered. + */ + fun getAnswerTextWatcher(): TextWatcher { + return object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(answer: CharSequence, start: Int, before: Int, count: Int) { + answerText = answer.toString().trim() + val isAnswerTextAvailable = answerText.isNotEmpty() + if (isAnswerTextAvailable != isAnswerAvailable.get()) { + isAnswerAvailable.set(isAnswerTextAvailable) + } + checkPendingAnswerError(AnswerErrorCategory.REAL_TIME) + } + + override fun afterTextChanged(s: Editable) { + } + } + } + + private fun deriveHintText(interaction: Interaction): CharSequence { + // The subtitled unicode can apparently exist in the structure in two different formats. + if (interactionType.hasPlaceholder) { + val placeholderUnicodeOption1 = + interaction.customizationArgsMap["placeholder"]?.subtitledUnicode + val placeholderUnicodeOption2 = + interaction.customizationArgsMap["placeholder"]?.customSchemaValue?.subtitledUnicode + val customPlaceholder1 = + placeholderUnicodeOption1?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" + val customPlaceholder2 = + placeholderUnicodeOption2?.let { unicode -> + translationController.extractString(unicode, writtenTranslationContext) + } ?: "" + return when { + customPlaceholder1.isNotEmpty() -> customPlaceholder1 + customPlaceholder2.isNotEmpty() -> customPlaceholder2 + else -> resourceHandler.getStringInLocale(interactionType.defaultHintTextStringId) + } + } else return resourceHandler.getStringInLocale(interactionType.defaultHintTextStringId) + } + + private fun retrieveAllowedVariables(interaction: Interaction): List { + return if (interactionType.hasCustomVariables) { + interaction.customizationArgsMap["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } else listOf() + } + + /** + * Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. Note that + * instances of this class must be created by injecting [FactoryFactoryImpl]. + */ + class FactoryImpl private constructor( + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController, + private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + private val interactionType: InteractionType + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return MathExpressionInteractionsViewModel( + interaction, + hasConversationView, + answerErrorReceiver, + writtenTranslationContext, + resourceHandler, + translationController, + mathExpressionAccessibilityUtil, + interactionType + ) + } + + /** A factory for [FactoryImpl]s based on for which interaction the factory is needed. */ + class FactoryFactoryImpl @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController, + private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil + ) { + /** Returns a new instance of [FactoryImpl] for NumericExpressionInput. */ + fun createFactoryForNumericExpression(): InteractionItemFactory { + return FactoryImpl( + resourceHandler, + translationController, + mathExpressionAccessibilityUtil, + InteractionType.NUMERIC_EXPRESSION + ) + } + + /** Returns a new instance of [FactoryImpl] for AlgebraicExpressionInput. */ + fun createFactoryForAlgebraicExpression(): InteractionItemFactory { + return FactoryImpl( + resourceHandler, + translationController, + mathExpressionAccessibilityUtil, + InteractionType.ALGEBRAIC_EXPRESSION + ) + } + + /** Returns a new instance of [FactoryImpl] for MathEquationInput. */ + fun createFactoryForMathEquation(): InteractionItemFactory { + return FactoryImpl( + resourceHandler, + translationController, + mathExpressionAccessibilityUtil, + InteractionType.MATH_EQUATION + ) + } + } + } + + private companion object { + private enum class InteractionType( + val viewType: ViewType, + @StringRes val defaultHintTextStringId: Int, + val hasPlaceholder: Boolean, + val hasCustomVariables: Boolean + ) { + /** Defines the view model behaviors corresponding to numeric expressions. */ + NUMERIC_EXPRESSION( + ViewType.NUMERIC_EXPRESSION_INPUT_INTERACTION, + defaultHintTextStringId = R.string.numeric_expression_default_hint_text, + hasPlaceholder = true, + hasCustomVariables = false + ) { + override fun computeLatex( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List + ): String? { + return parseAnswer(answerText, allowedVariables) + .getResult() + ?.toRawLatex(useFractionsForDivision) + } + + override fun computeHumanReadableString( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + language: OppiaLanguage + ): String? { + return parseAnswer(answerText, allowedVariables).getResult()?.let { exp -> + mathExpressionAccessibilityUtil.convertToHumanReadableString( + exp, language, useFractionsForDivision + ) + } + } + + override fun parseAnswer( + answerText: String, + allowedVariables: List + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(answerText) + } + }, + + /** Defines the view model behaviors corresponding to algebraic expressions. */ + ALGEBRAIC_EXPRESSION( + ViewType.ALGEBRAIC_EXPRESSION_INPUT_INTERACTION, + defaultHintTextStringId = R.string.algebraic_expression_default_hint_text, + hasPlaceholder = false, + hasCustomVariables = true + ) { + override fun computeLatex( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List + ): String? { + return parseAnswer(answerText, allowedVariables) + .getResult() + ?.toRawLatex(useFractionsForDivision) + } + + override fun computeHumanReadableString( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + language: OppiaLanguage + ): String? { + return parseAnswer(answerText, allowedVariables).getResult()?.let { exp -> + mathExpressionAccessibilityUtil.convertToHumanReadableString( + exp, language, useFractionsForDivision + ) + } + } + + override fun parseAnswer( + answerText: String, + allowedVariables: List + ): MathParsingResult = + MathExpressionParser.parseAlgebraicExpression(answerText, allowedVariables) + }, + + /** Defines the view model behaviors corresponding to math equations. */ + MATH_EQUATION( + ViewType.MATH_EQUATION_INPUT_INTERACTION, + defaultHintTextStringId = R.string.math_equation_default_hint_text, + hasPlaceholder = false, + hasCustomVariables = true + ) { + override fun computeLatex( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List + ): String? { + return parseAnswer(answerText, allowedVariables) + .getResult() + ?.toRawLatex(useFractionsForDivision) + } + + override fun computeHumanReadableString( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + language: OppiaLanguage + ): String? { + return parseAnswer(answerText, allowedVariables).getResult()?.let { exp -> + mathExpressionAccessibilityUtil.convertToHumanReadableString( + exp, language, useFractionsForDivision + ) + } + } + + override fun parseAnswer( + answerText: String, + allowedVariables: List + ): MathParsingResult = + MathExpressionParser.parseAlgebraicEquation(answerText, allowedVariables) + }; + + /** + * Computes and returns the human-readable error corresponding to the specified answer and + * context, or null if there the answer has no errors. + */ + fun computeSubmitTimeError( + answerText: String, + allowedVariables: List, + appLanguageResourceHandler: AppLanguageResourceHandler + ): String? { + return when (val parseResult = parseAnswer(answerText, allowedVariables)) { + is MathParsingResult.Failure -> when (val error = parseResult.error) { + is DisabledVariablesInUseError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_invalid_variable, + error.variables.joinToString(separator = ", ") + ) + } + EquationIsMissingEqualsError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_missing_equals + ) + } + EquationHasTooManyEqualsError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_more_than_one_equals + ) + } + EquationMissingLhsOrRhsError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_hanging_equals + ) + } + ExponentIsVariableExpressionError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_exponent_has_variable + ) + } + ExponentTooLargeError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_exponent_too_large + ) + } + FunctionNameIncompleteError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_incomplete_function_name + ) + } + GenericError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_generic + ) + } + HangingSquareRootError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_hanging_square_root + ) + } + is InvalidFunctionInUseError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_unsupported_function, error.functionName + ) + } + is MultipleRedundantParenthesesError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_multiple_redundant_parentheses, error.rawExpression + ) + } + NestedExponentsError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_nested_exponent + ) + } + is NoVariableOrNumberAfterBinaryOperatorError -> when (error.operator) { + UnaryOperator.ADD -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_rhs_for_addition_operator, + error.operatorSymbol + ) + } + UnaryOperator.SUBTRACT -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_rhs_for_subtraction_operator, + error.operatorSymbol + ) + } + UnaryOperator.MULTIPLY -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_rhs_for_multiplication_operator, + error.operatorSymbol + ) + } + UnaryOperator.DIVIDE -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_rhs_for_division_operator, + error.operatorSymbol + ) + } + UnaryOperator.EXPONENTIATE -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_rhs_for_exponentiation_operator, + error.operatorSymbol + ) + } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_generic + ) + } + } + is NoVariableOrNumberBeforeBinaryOperatorError -> when (error.operator) { + UnaryOperator.ADD -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_lhs_for_addition_operator, + error.operatorSymbol + ) + } + // Subtraction can't happen since these cases are treated as negation. + UnaryOperator.SUBTRACT -> error("This case should never happen.") + UnaryOperator.MULTIPLY -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_lhs_for_multiplication_operator, + error.operatorSymbol + ) + } + UnaryOperator.DIVIDE -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_lhs_for_division_operator, + error.operatorSymbol + ) + } + UnaryOperator.EXPONENTIATE -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_missing_lhs_for_exponentiation_operator, + error.operatorSymbol + ) + } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_generic + ) + } + } + is NumberAfterVariableError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_number_after_var_term, + error.variable, + error.number.toPlainText() + ) + } + is RedundantParenthesesForIndividualTermsError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_redundant_parentheses_individual_term, + error.rawExpression + ) + } + is SingleRedundantParenthesesError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_single_redundant_parentheses, error.rawExpression + ) + } + SpacesBetweenNumbersError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_spaces_in_numerical_input + ) + } + is SubsequentBinaryOperatorsError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_consecutive_binary_operators, + error.operator1, + error.operator2 + ) + } + is SubsequentUnaryOperatorsError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_consecutive_unary_operators + ) + } + TermDividedByZeroError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_term_divided_by_zero + ) + } + UnbalancedParenthesesError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_unbalanced_parentheses + ) + } + is UnnecessarySymbolsError -> { + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.math_expression_error_unnecessary_symbols, error.invalidSymbol + ) + } + VariableInNumericExpressionError -> { + appLanguageResourceHandler.getStringInLocale( + R.string.math_expression_error_variable_in_numeric_expression + ) + } + } + is MathParsingResult.Success -> null // No errors. + } + } + + /** + * Returns the LaTeX representation of the specified answer with potential customization for + * treating divisions as fractions per [useFractionsForDivision]. + */ + abstract fun computeLatex( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List + ): String? + + /** + * Returns the human-readable accessibility string corresponding to the specified answer with + * potential customization for treating divisions as fractions per [useFractionsForDivision]. + */ + abstract fun computeHumanReadableString( + answerText: String, + useFractionsForDivision: Boolean, + allowedVariables: List, + mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, + language: OppiaLanguage + ): String? + + /** Attempts to parse the provided raw answer and return the [MathParsingResult]. */ + protected abstract fun parseAnswer( + answerText: String, + allowedVariables: List + ): MathParsingResult<*> + + protected companion object { + /** + * Returns the successful result from this [MathParsingResult] or null if it's a failure. + */ + fun MathParsingResult.getResult(): T? = when (this) { + is MathParsingResult.Success -> result + is MathParsingResult.Failure -> null + } + } + } + } +} 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 4c34453e937..70deff47224 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 @@ -4,6 +4,7 @@ import android.text.Editable import android.text.TextWatcher import androidx.databinding.Observable 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 @@ -11,10 +12,12 @@ 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 import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject /** [StateItemViewModel] for the numeric input interaction. */ -class NumericInputViewModel( +class NumericInputViewModel private constructor( val hasConversationView: Boolean, private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length val isSplitView: Boolean, @@ -86,4 +89,28 @@ class NumericInputViewModel( this.writtenTranslationContext = this@NumericInputViewModel.writtenTranslationContext } }.build() + + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return NumericInputViewModel( + hasConversationView, + answerErrorReceiver, + isSplitView, + writtenTranslationContext, + resourceHandler + ) + } + } } 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 064b7fc3f60..749151c4c40 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 @@ -13,13 +13,15 @@ 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.player.state.answerhandling.InteractionAnswerReceiver 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 +import org.oppia.android.util.math.toAnswerString +import javax.inject.Inject /** [StateItemViewModel] for the ratio expression input interaction. */ -class RatioExpressionInputInteractionViewModel( +class RatioExpressionInputInteractionViewModel private constructor( interaction: Interaction, val hasConversationView: Boolean, val isSplitView: Boolean, @@ -124,4 +126,31 @@ class RatioExpressionInputInteractionViewModel( else -> resourceHandler.getStringInLocale(R.string.ratio_default_hint_text) } } + + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler, + private val translationController: TranslationController + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return RatioExpressionInputInteractionViewModel( + interaction, + hasConversationView, + isSplitView, + answerErrorReceiver, + writtenTranslationContext, + resourceHandler, + translationController + ) + } + } } 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 f86578a3c7d..21976f2a971 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 @@ -12,8 +12,10 @@ 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 +import javax.inject.Inject /** Corresponds to the type of input that should be used for an item selection interaction view. */ enum class SelectionItemInputType { @@ -22,7 +24,7 @@ enum class SelectionItemInputType { } /** [StateItemViewModel] for multiple or item-selection input choice list. */ -class SelectionInteractionViewModel( +class SelectionInteractionViewModel private constructor( val entityId: String, val hasConversationView: Boolean, interaction: Interaction, @@ -157,6 +159,32 @@ class SelectionInteractionViewModel( } } + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val translationController: TranslationController + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return SelectionInteractionViewModel( + entityId, + hasConversationView, + interaction, + answerErrorReceiver, + isSplitView, + writtenTranslationContext, + translationController + ) + } + } + companion object { private fun computeChoiceItems( choiceSubtitledHtmls: List, diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SplitScreenInteractionIds.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SplitScreenInteractionIds.kt new file mode 100644 index 00000000000..244d58fa46d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SplitScreenInteractionIds.kt @@ -0,0 +1,8 @@ +package org.oppia.android.app.player.state.itemviewmodel + +import javax.inject.Qualifier + +/** + * Corresponds to an injectable set of string interaction IDs that support split-screen variants. + */ +@Qualifier annotation class SplitScreenInteractionIds diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SplitScreenInteractionModule.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SplitScreenInteractionModule.kt new file mode 100644 index 00000000000..b9ab2f588da --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SplitScreenInteractionModule.kt @@ -0,0 +1,19 @@ +package org.oppia.android.app.player.state.itemviewmodel + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet + +/** Module to define which interactions support split-screen versions. */ +@Module +class SplitScreenInteractionModule { + @Provides + @IntoSet + @SplitScreenInteractionIds + fun provideDragAndDropSortInputSplitScreenSupportIndication(): String = "DragAndDropSortInput" + + @Provides + @IntoSet + @SplitScreenInteractionIds + fun provideImageClickInputSplitScreenSupportIndication(): String = "ImageClickInput" +} diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt index cce34492ed6..bce47761e47 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt @@ -1,5 +1,9 @@ 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 import org.oppia.android.app.viewmodel.ObservableViewModel /** @@ -27,5 +31,28 @@ abstract class StateItemViewModel(val viewType: ViewType) : ObservableViewModel( DRAG_DROP_SORT_INTERACTION, IMAGE_REGION_SELECTION_INTERACTION, RATIO_EXPRESSION_INPUT_INTERACTION, + NUMERIC_EXPRESSION_INPUT_INTERACTION, + ALGEBRAIC_EXPRESSION_INPUT_INTERACTION, + MATH_EQUATION_INPUT_INTERACTION + } + + /** Factory for creating new [StateItemViewModel]s for interactions. */ + interface InteractionItemFactory { + /** + * Returns a new [StateItemViewModel] corresponding to this interaction with the GCS entity ID, + * the [Interaction] object corresponding to the interaction view, a receiver for answers if this + * interaction pushes answers, and whether there's a previous button enabled (only relevant for + * navigation-based interactions). + */ + fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SubmittedAnswerViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SubmittedAnswerViewModel.kt index 704b25833d1..9e90beb624e 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SubmittedAnswerViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SubmittedAnswerViewModel.kt @@ -25,7 +25,7 @@ class SubmittedAnswerViewModel( ) private var accessibleAnswer: String? = DEFAULT_ACCESSIBLE_ANSWER - fun setSubmittedAnswer(submittedAnswer: CharSequence, accessibleAnswer: String? = null) { + fun setSubmittedAnswer(submittedAnswer: CharSequence, accessibleAnswer: String?) { this.submittedAnswer.set(submittedAnswer) this.accessibleAnswer = accessibleAnswer updateSubmittedAnswerContentDescription() 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 821faa8676d..4afebaccc72 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 @@ -10,10 +10,12 @@ 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.domain.translation.TranslationController +import javax.inject.Inject /** [StateItemViewModel] for the text input interaction. */ -class TextInputViewModel( +class TextInputViewModel private constructor( interaction: Interaction, val hasConversationView: Boolean, private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length @@ -84,4 +86,29 @@ class TextInputViewModel( } ?: "" // The default placeholder for text input is empty. return if (placeholder1.isNotEmpty()) placeholder1 else placeholder2 } + + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ + class FactoryImpl @Inject constructor( + private val translationController: TranslationController + ) : InteractionItemFactory { + override fun create( + entityId: String, + hasConversationView: Boolean, + interaction: Interaction, + interactionAnswerReceiver: InteractionAnswerReceiver, + answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, + hasPreviousButton: Boolean, + isSplitView: Boolean, + writtenTranslationContext: WrittenTranslationContext + ): StateItemViewModel { + return TextInputViewModel( + interaction, + hasConversationView, + answerErrorReceiver, + isSplitView, + writtenTranslationContext, + translationController + ) + } + } } 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 3ca8d4771b4..9929a4077ee 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 @@ -19,6 +19,7 @@ import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject private const val TEST_ACTIVITY_TAG = "TestActivity" @@ -96,24 +97,20 @@ class StateFragmentTestActivityPresenter @Inject constructor( explorationId, shouldSavePartialProgress, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - .observe( - activity, - Observer> { result -> - when { - result.isPending() -> oppiaLogger.d(TEST_ACTIVITY_TAG, "Loading exploration") - result.isFailure() -> oppiaLogger.e( - TEST_ACTIVITY_TAG, - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { - oppiaLogger.d(TEST_ACTIVITY_TAG, "Successfully loaded exploration") - initializeExploration(profileId, topicId, storyId, explorationId) - } + ).toLiveData().observe( + activity, + Observer> { result -> + when (result) { + is AsyncResult.Pending -> oppiaLogger.d(TEST_ACTIVITY_TAG, "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e(TEST_ACTIVITY_TAG, "Failed to load exploration", result.error) + is AsyncResult.Success -> { + oppiaLogger.d(TEST_ACTIVITY_TAG, "Successfully loaded exploration") + initializeExploration(profileId, topicId, storyId, explorationId) } } - ) + } + ) } /** diff --git a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt index ce0a67e81db..82a0ad1a982 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt @@ -274,26 +274,30 @@ class AddProfileActivityPresenter @Inject constructor( result: AsyncResult, binding: AddProfileActivityBinding ) { - if (result.isSuccess()) { - val intent = Intent(activity, ProfileChooserActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - activity.startActivity(intent) - } else if (result.isFailure()) { - when (result.getErrorOrNull()) { - is ProfileManagementController.ProfileNameNotUniqueException -> - profileViewModel.nameErrorMsg.set( - resourceHandler.getStringInLocale( - R.string.add_profile_error_name_not_unique + when (result) { + is AsyncResult.Success -> { + val intent = Intent(activity, ProfileChooserActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity.startActivity(intent) + } + is AsyncResult.Failure -> { + when (result.error) { + is ProfileManagementController.ProfileNameNotUniqueException -> + profileViewModel.nameErrorMsg.set( + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_not_unique + ) ) - ) - is ProfileManagementController.ProfileNameOnlyLettersException -> - profileViewModel.nameErrorMsg.set( - resourceHandler.getStringInLocale( - R.string.add_profile_error_name_only_letters + is ProfileManagementController.ProfileNameOnlyLettersException -> + profileViewModel.nameErrorMsg.set( + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_only_letters + ) ) - ) + } + binding.addProfileActivityScrollView.smoothScrollTo(0, 0) } - binding.addProfileActivityScrollView.smoothScrollTo(0, 0) + is AsyncResult.Pending -> {} // Wait for an actual result. } } diff --git a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt index aae69e73de2..8a7a082abb1 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt @@ -15,6 +15,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.AdminPinActivityBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -114,7 +115,7 @@ class AdminPinActivityPresenter @Inject constructor( profileManagementController.updatePin(profileId, inputPin).toLiveData().observe( activity, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { when (activity.intent.getIntExtra(ADMIN_PIN_ENUM_EXTRA_KEY, 0)) { AdminAuthEnum.PROFILE_ADMIN_CONTROLS.value -> { activity.startActivity( diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt index b8d4b3c8dab..1894391df81 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt @@ -18,6 +18,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.PinPasswordActivityBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -88,7 +89,7 @@ class PinPasswordActivityPresenter @Inject constructor( .observe( activity, { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { activity.startActivity((HomeActivity.createHomeActivity(activity, profileId))) } } diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt index d7d111158ba..edd0ba366ac 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt @@ -48,14 +48,14 @@ class PinPasswordViewModel @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "PinPasswordActivity", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("PinPasswordActivity", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - val profile = profileResult.getOrDefault(Profile.getDefaultInstance()) correctPin.set(profile.pin) isAdmin.set(profile.isAdmin) name.set(profile.name) diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt index 36f18e466fc..9361215f5c7 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt @@ -124,14 +124,18 @@ class ProfileChooserFragmentPresenter @Inject constructor( private fun processWasProfileEverBeenAddedResult( wasProfileEverBeenAddedResult: AsyncResult ): Boolean { - if (wasProfileEverBeenAddedResult.isFailure()) { - oppiaLogger.e( - "ProfileChooserFragment", - "Failed to retrieve the information on wasProfileEverBeenAdded", - wasProfileEverBeenAddedResult.getErrorOrNull()!! - ) + return when (wasProfileEverBeenAddedResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileChooserFragment", + "Failed to retrieve the information on wasProfileEverBeenAdded", + wasProfileEverBeenAddedResult.error + ) + false + } + is AsyncResult.Pending -> false + is AsyncResult.Success -> wasProfileEverBeenAddedResult.value } - return wasProfileEverBeenAddedResult.getOrDefault(/* defaultValue= */ false) } /** Randomly selects a color for the new profile that is not already in use. */ @@ -175,7 +179,7 @@ class ProfileChooserFragmentPresenter @Inject constructor( profileManagementController.loginToProfile(model.profile.id).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { activity.startActivity( ( HomeActivity.createHomeActivity( diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt index 217a72fcbb1..d78a40767e7 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt @@ -42,14 +42,16 @@ class ProfileChooserViewModel @Inject constructor( private fun processGetProfilesResult( profilesResult: AsyncResult> ): List { - if (profilesResult.isFailure()) { - oppiaLogger.e( - "ProfileChooserViewModel", - "Failed to retrieve the list of profiles", - profilesResult.getErrorOrNull()!! - ) - } - val profileList = profilesResult.getOrDefault(emptyList()).map { + val profileList = when (profilesResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileChooserViewModel", "Failed to retrieve the list of profiles", profilesResult.error + ) + emptyList() + } + is AsyncResult.Pending -> emptyList() + is AsyncResult.Success -> profilesResult.value + }.map { ProfileChooserUiModel.newBuilder().setProfile(it).build() }.toMutableList() diff --git a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt index 852417f4263..0b6d5beb5b6 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt @@ -16,6 +16,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ResetPinDialogBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -95,7 +96,7 @@ class ResetPinDialogFragmentPresenter @Inject constructor( .observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { routeDialogInterface.routeToSuccessDialog() } } diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt index 6c3a8a2fb67..9a487c48a1a 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt @@ -80,14 +80,14 @@ class ProfilePictureActivityPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfilePictureActivity", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("ProfilePictureActivity", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun setProfileAvatar(avatar: ProfileAvatar) { diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt index e84fea15ab5..c6036860a9d 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt @@ -74,14 +74,14 @@ class ProfileProgressViewModel @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("ProfileProgressFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private val promotedActivityListResultLiveData: @@ -113,16 +113,20 @@ class ProfileProgressViewModel @Inject constructor( } private fun processPromotedActivityListResult( - promotedActivityListtResult: AsyncResult + promotedActivityListResult: AsyncResult ): PromotedActivityList { - if (promotedActivityListtResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve promoted story list: ", - promotedActivityListtResult.getErrorOrNull()!! - ) + return when (promotedActivityListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve promoted story list: ", + promotedActivityListResult.error + ) + PromotedActivityList.getDefaultInstance() + } + is AsyncResult.Pending -> PromotedActivityList.getDefaultInstance() + is AsyncResult.Success -> promotedActivityListResult.value } - return promotedActivityListtResult.getOrDefault(PromotedActivityList.getDefaultInstance()) } private fun processPromotedActivityList( @@ -169,14 +173,18 @@ class ProfileProgressViewModel @Inject constructor( private fun processGetCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve completed story list", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve completed story list", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun subscribeToOngoingTopicListLiveData() { @@ -198,13 +206,17 @@ class ProfileProgressViewModel @Inject constructor( private fun processGetOngoingTopicListResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve ongoing topic list", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve ongoing topic list", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt index 16cc17775b9..f49c2081f62 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt @@ -139,14 +139,18 @@ class ResumeLessonFragmentPresenter @Inject constructor( private fun processChapterSummaryResult( chapterSummaryResult: AsyncResult ): ChapterSummary { - if (chapterSummaryResult.isFailure()) { - oppiaLogger.e( - "ResumeLessonFragment", - "Failed to retrieve chapter summary for the explorationId $explorationId: ", - chapterSummaryResult.getErrorOrNull() - ) + return when (chapterSummaryResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ResumeLessonFragment", + "Failed to retrieve chapter summary for the explorationId $explorationId: ", + chapterSummaryResult.error + ) + ChapterSummary.getDefaultInstance() + } + is AsyncResult.Pending -> ChapterSummary.getDefaultInstance() + is AsyncResult.Success -> chapterSummaryResult.value } - return chapterSummaryResult.getOrDefault(ChapterSummary.getDefaultInstance()) } private fun playExploration( @@ -166,17 +170,14 @@ class ResumeLessonFragmentPresenter @Inject constructor( // ResumeLessonFragment implies that learner has not completed the lesson. shouldSavePartialProgress = true, explorationCheckpoint - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("ResumeLessonFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "ResumeLessonFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("ResumeLessonFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("ResumeLessonFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("ResumeLessonFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt index 9839f15b8a8..10bc638a31b 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt @@ -14,6 +14,7 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.databinding.ProfileEditFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -96,11 +97,9 @@ class ProfileEditFragmentPresenter @Inject constructor( ).toLiveData().observe( activity, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( - "ProfileEditActivityPresenter", - "Failed to updated allow download access", - it.getErrorOrNull()!! + "ProfileEditActivityPresenter", "Failed to updated allow download access", it.error ) } } @@ -126,7 +125,7 @@ class ProfileEditFragmentPresenter @Inject constructor( .observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { if (fragment.requireContext().resources.getBoolean(R.bool.isTablet)) { val intent = Intent(fragment.requireContext(), AdministratorControlsActivity::class.java) diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt index 549c3be7798..75a3a98b5dd 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt @@ -44,14 +44,18 @@ class ProfileEditViewModel @Inject constructor( /** Fetches the profile of a user asynchronously. */ private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfileEditViewModel", - "Failed to retrieve the profile with ID: ${profileId.internalId}", - profileResult.getErrorOrNull()!! - ) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileEditViewModel", + "Failed to retrieve the profile with ID: ${profileId.internalId}", + profileResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - val profile = profileResult.getOrDefault(Profile.getDefaultInstance()) isAllowedDownloadAccessMutableLiveData.value = profile.allowDownloadAccess isAdmin = profile.isAdmin return profile diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt index a8b82176459..dfa4e50e6fe 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt @@ -28,14 +28,16 @@ class ProfileListViewModel @Inject constructor( } private fun processGetProfilesResult(profilesResult: AsyncResult>): List { - if (profilesResult.isFailure()) { - oppiaLogger.e( - "ProfileListViewModel", - "Failed to retrieve the list of profiles", - profilesResult.getErrorOrNull()!! - ) + val profileList = when (profilesResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileListViewModel", "Failed to retrieve the list of profiles", profilesResult.error + ) + emptyList() + } + is AsyncResult.Pending -> emptyList() + is AsyncResult.Success -> profilesResult.value } - val profileList = profilesResult.getOrDefault(emptyList()) val sortedProfileList = profileList.sortedBy { machineLocale.run { it.name.toMachineLowerCase() } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt index 7ab38ce0f16..6350c2e4581 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt @@ -101,12 +101,12 @@ class ProfileRenameFragmentPresenter @Inject constructor( } private fun handleAddProfileResult(result: AsyncResult, profileId: Int) { - if (result.isSuccess()) { + if (result is AsyncResult.Success) { val intent = ProfileEditActivity.createProfileEditActivity(activity, profileId) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) activity.startActivity(intent) - } else if (result.isFailure()) { - when (result.getErrorOrNull()) { + } else if (result is AsyncResult.Failure) { + when (result.error) { is ProfileManagementController.ProfileNameNotUniqueException -> renameViewModel.nameErrorMsg.set( resourceHandler.getStringInLocale( diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt index da948172c4c..b4afe1d73e1 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt @@ -16,6 +16,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ProfileResetPinFragmentBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -133,7 +134,7 @@ class ProfileResetPinFragmentPresenter @Inject constructor( .observe( activity, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { val intent = ProfileEditActivity.createProfileEditActivity(activity, profileId) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) activity.startActivity(intent) diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt index 8d779fb14fd..84fe06a1197 100644 --- a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt @@ -115,16 +115,15 @@ class SplashActivityPresenter @Inject constructor( private fun processInitState( initStateResult: AsyncResult ): SplashInitState { - if (initStateResult.isFailure()) { - oppiaLogger.e( - "SplashActivity", - "Failed to compute initial state", - initStateResult.getErrorOrNull() - ) - } - // If there's an error loading the data, assume the default. - return initStateResult.getOrDefault(SplashInitState.computeDefault(localeController)) + return when (initStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("SplashActivity", "Failed to compute initial state", initStateResult.error) + SplashInitState.computeDefault(localeController) + } + is AsyncResult.Pending -> SplashInitState.computeDefault(localeController) + is AsyncResult.Success -> initStateResult.value + } } private fun getDeprecationNoticeDialogFragment(): AutomaticAppDeprecationNoticeDialogFragment? { diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index fd52ed1ee7e..9c03a1b5b48 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -35,6 +35,7 @@ import org.oppia.android.databinding.StoryHeaderViewBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.HtmlParser import org.oppia.android.util.parser.html.TopicHtmlParserEntityType @@ -273,17 +274,14 @@ class StoryFragmentPresenter @Inject constructor( shouldSavePartialProgress = shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("Story Fragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "Story Fragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("Story Fragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("Story Fragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("Story Fragment", "Successfully loaded exploration: $explorationId") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt index 6b7970a08cf..c127eae3707 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt @@ -70,15 +70,14 @@ class StoryViewModel @Inject constructor( } private fun processStoryResult(storyResult: AsyncResult): StorySummary { - if (storyResult.isFailure()) { - oppiaLogger.e( - "StoryFragment", - "Failed to retrieve Story: ", - storyResult.getErrorOrNull()!! - ) + return when (storyResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("StoryFragment", "Failed to retrieve Story: ", storyResult.error) + StorySummary.getDefaultInstance() + } + is AsyncResult.Pending -> StorySummary.getDefaultInstance() + is AsyncResult.Success -> storyResult.value } - - return storyResult.getOrDefault(StorySummary.getDefaultInstance()) } private fun processStoryChapterList(storySummary: StorySummary): List { diff --git a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt index 10bee4d6ddc..db9186002dd 100644 --- a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt @@ -56,7 +56,7 @@ class StoryChapterSummaryViewModel( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( internalProfileId, @@ -66,9 +66,9 @@ class StoryChapterSummaryViewModel( canExplorationBeResumed = true, shouldSavePartialProgress, backflowId = 1, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt index a1793b20654..0eab42a60a2 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivity.kt @@ -6,17 +6,25 @@ import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.home.RouteToExplorationListener import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.topic.TopicFragment +import org.oppia.android.app.utility.SplitScreenManager import javax.inject.Inject /** The activity for testing [TopicFragment]. */ class ExplorationTestActivity : InjectableAppCompatActivity(), RouteToExplorationListener { @Inject - lateinit var explorationTestActivityPresenter: ExplorationTestActivityPresenter + lateinit var presenter: ExplorationTestActivityPresenter + + /** + * Exposes the [SplitScreenManager] corresponding to the fragment under test for tests to interact + * with. + */ + val splitScreenManager: SplitScreenManager + get() = getTestFragment().splitScreenManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - explorationTestActivityPresenter.handleOnCreate() + presenter.handleOnCreate() } override fun routeToExploration( @@ -39,4 +47,9 @@ class ExplorationTestActivity : InjectableAppCompatActivity(), RouteToExploratio ) ) } + + private fun getTestFragment() = checkNotNull(presenter.getTestFragment()) { + "Expected TestFragment to be present in inflated test activity. Did you try to retrieve the" + + " screen manager too early in the test?" + } } diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt index 36ae33e2d6e..2b916b46d2c 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt @@ -1,18 +1,23 @@ package org.oppia.android.app.testing +import android.content.Context import android.widget.Button import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment import org.oppia.android.app.home.RouteToExplorationListener import org.oppia.android.app.model.ExplorationCheckpoint +import org.oppia.android.app.utility.SplitScreenManager import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject private const val INTERNAL_PROFILE_ID = 0 @@ -20,6 +25,7 @@ private const val TOPIC_ID = TEST_TOPIC_ID_0 private const val STORY_ID = TEST_STORY_ID_0 private const val EXPLORATION_ID = TEST_EXPLORATION_ID_2 private const val TAG_EXPLORATION_TEST_ACTIVITY = "ExplorationTestActivity" +private const val TEST_FRAGMENT_TAG = "ExplorationTestActivity.TestFragment" /** The presenter for [ExplorationTestActivityPresenter]. */ @ActivityScope @@ -33,6 +39,9 @@ class ExplorationTestActivityPresenter @Inject constructor( fun handleOnCreate() { activity.setContentView(R.layout.exploration_test_activity) + activity.supportFragmentManager.beginTransaction().apply { + add(R.id.exploration_test_fragment_placeholder, TestFragment(), TEST_FRAGMENT_TAG) + }.commitNow() activity.findViewById