From a23de658e485919f00e3a9603f1781f4c087185f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 6 Nov 2019 23:22:57 -0800 Subject: [PATCH] Fix #259 and part of #163: Finish state fragment overall UI structure (#270) * Attempt to rebase content-card onto develop. * Manually apply rejections and remove rej files. * Add missing pieces implied in merge-fix that are needed for this branch. * Maunally apply multiple-single-input-interaction changes to develop branch (with patch rejections). * Manually apply rejected diffs. * Fix broken build after merges. * Add missing changes from merge-fix that are needed to enable state content binding. * Resolve unresolved conflicts. * Make an exploration with all prototype interactions work end-to-end. This includes: - Introducing an exploration with all prototype interactions - Introducing support for binding all interaction types - Fixing some parsing issues with fractions - Rewiring the interaction object creation for views to be generic - Removing temporary answer handling everywhere - Other random fixes This includes no tests, and there are a few other fixes still necessary for the core learning experience to fully work. * Add support for displaying feedback items. * Add support for reshowing submitted answers. * Make 'Continue' button a separate interaction (but keep the existing general 'Continue' navigation button). Rework how navigation buttons behave. Introduce automatic answer submission for some interactions (Continue, ItemSelection if it shows radio buttons, and MultipleChoice). * Move audio functionality to a custom toolbar, remove the dummy audio button, and remove the transient state name at the top of the state fragment. * Simplify the presenter by updating ExplorationProgressController to feed information about the next and current state to allow the state presenter to only focus on rendering the current state. * First batch of work corresponding to moving the entirety of state fragment's recycler view (and child views/recycler view) to be bound via data-binding. * Finish updating item selection recycler view to use data binding. This required introducing a ViewComponent that inherits from FragmentComponent in the Dagger dependency graph. This also involved updating the app to use a newer AndroidX fragment library that's still pre-release in order to gain access to the necessary functionality to associate views with their fragments. The two custom adapters have been removed in this change. * Some minor fixes to polish up the experience a bit and reduce jank. * Fix broken voiceover audio button. * Address one TODO to generalize adding interactions by leveraging a Dagger module. * Fix broken test. * Fix broken domain tests by allowing feedback to be either a string or an object, and loading about_oppia instead of oppia_exploration since the former is expected by ExplorationProgressControllerTest. * Fix broken app test builds (tests still fail, but at least run now). * Add TODO to fix feedback parsing hack. * Remove TODOs in StringToFractionParser. * Resolve open TODOs for the state portion of the player, or associate them with issues. * Associate 2 more TODOs with issues. * Add tests for new next state behavior in ExplorationProgressController, and resolve last unattributed TODO. * Remove number with units support. * Post-merge fixes and clean-ups. * Post-merge fix. * Remove number with units tests. * First round of addressing reviewer comments. * Remove view scope & component. * Second round of addressing reviewer comments. Includes a project-wide optimization of all imports. * Round 3 of addressing reviewer changes: removed UrlImageParserTest since it was pulled in with earlier, unfinalized changes and we have since realized it's not possible to test the image parser based on the current project setup. * Round 4 of addressing reviewer comments: reformat all layout XML files. * Round 5 of addressing reviewer comments. * Revert "Remove view scope & component." This reverts commit 99783d53bae9d4233ef5c2de96f17a1a2499ecd8. * Post-clean up fixes and other minor adjustments. * Ensure ExplorationActivityTest passes even though it doesn't have interesting tests yet. * Temporary workaround to make the prototype exploration accessible with recent topic & home fragment changes. Also, hide the topic button in the home fragment since the topic is accessible directly via the home fragment tiles. * Round 6 of addressing review comments. * Update prototype exploration to provide feedback after the multi-item item selection state for the correct answer so that the player doesn't seem broken. --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 4 +- .../oppia/app/activity/ActivityComponent.kt | 2 +- .../app/application/ApplicationComponent.kt | 4 +- .../FractionInputInteractionView.kt | 13 +- .../interaction/InteractionAnswerRetriever.kt | 8 - .../NumericInputInteractionView.kt | 12 +- .../interaction/TextInputInteractionView.kt | 12 +- .../databinding/ImageViewBindingAdapters.kt | 2 +- .../oppia/app/fragment/FragmentComponent.kt | 9 +- .../org/oppia/app/fragment/FragmentModule.kt | 7 + .../app/fragment/InjectableDialogFragment.kt | 2 +- .../oppia/app/fragment/InjectableFragment.kt | 8 +- .../java/org/oppia/app/home/HomeFragment.kt | 2 +- .../oppia/app/home/HomeFragmentPresenter.kt | 5 +- .../ContinuePlayingFragment.kt | 2 +- .../home/topiclist/PromotedStoryViewModel.kt | 2 +- .../home/topiclist/TopicSummaryViewModel.kt | 1 - .../app/parser/StringToFractionParser.kt | 72 +- .../oppia/app/player/audio/AudioFragment.kt | 2 +- .../player/audio/AudioFragmentPresenter.kt | 1 - .../audio/CellularDataDialogFragment.kt | 2 +- .../ExplorationActivityPresenter.kt | 12 +- .../player/exploration/ExplorationFragment.kt | 4 +- .../ExplorationFragmentPresenter.kt | 5 +- .../app/player/state/InteractionAdapter.kt | 200 ------ .../player/state/SelectInputItemsListener.kt | 8 - .../player/state/SelectionInteractionView.kt | 112 ++++ .../oppia/app/player/state/StateAdapter.kt | 189 ------ .../oppia/app/player/state/StateFragment.kt | 32 +- .../player/state/StateFragmentPresenter.kt | 498 +++++++------- .../oppia/app/player/state/StateViewModel.kt | 28 +- .../InteractionAnswerHandler.kt | 27 + .../SelectionInputInteractionView.kt | 43 -- .../state/itemviewmodel/ContentViewModel.kt | 10 +- .../ContinueInteractionViewModel.kt | 28 + .../state/itemviewmodel/FeedbackViewModel.kt | 4 + .../FractionInteractionViewModel.kt | 20 + .../InteractionViewModelFactory.kt | 15 + .../InteractionViewModelModule.kt | 59 ++ .../itemviewmodel/NumericInputViewModel.kt | 19 + .../SelectionInteractionContentViewModel.kt | 21 +- ...onInteractionCustomizationArgsViewModel.kt | 11 - .../SelectionInteractionViewModel.kt | 123 ++++ .../itemviewmodel/StateButtonViewModel.kt | 108 --- .../state/itemviewmodel/StateItemViewModel.kt | 6 + .../StateNavigationButtonViewModel.kt | 100 +++ .../state/itemviewmodel/TextInputViewModel.kt | 19 + .../listener/InputInteractionListener.kt | 9 - .../listener/InteractionAnswerRetriever.kt | 8 - .../state/listener/ItemClickListener.kt | 8 - ...er.kt => StateNavigationButtonListener.kt} | 6 +- .../oppia/app/profile/AddProfileFragment.kt | 2 +- .../oppia/app/profile/AdminAuthFragment.kt | 2 +- .../app/profile/AdminAuthFragmentPresenter.kt | 1 - .../app/profile/ProfileChooserFragment.kt | 2 +- .../ProfileChooserFragmentPresenter.kt | 1 - .../app/profile/ProfileChooserViewModel.kt | 1 - .../oppia/app/recyclerview/BindableAdapter.kt | 1 + .../RecyclerViewBindingAdapter.kt | 21 +- .../java/org/oppia/app/story/StoryFragment.kt | 2 +- .../testing/BindableAdapterTestFragment.kt | 2 +- .../app/testing/HtmlParserTestActivity.kt | 2 +- .../InputInteractionViewTestActivity.kt | 19 +- .../java/org/oppia/app/topic/TopicFragment.kt | 2 +- .../org/oppia/app/topic/ViewPagerAdapter.kt | 2 +- .../topic/conceptcard/ConceptCardFragment.kt | 2 +- .../topic/overview/TopicOverviewFragment.kt | 2 +- .../app/topic/play/ChapterSummaryAdapter.kt | 4 +- .../oppia/app/topic/play/TopicPlayFragment.kt | 2 +- .../questionplayer/QuestionPlayerFragment.kt | 2 +- .../review/ReviewSkillSelectionAdapter.kt | 2 +- .../app/topic/review/TopicReviewFragment.kt | 2 +- .../app/topic/train/TopicTrainFragment.kt | 2 +- .../java/org/oppia/app/view/ViewComponent.kt | 21 + .../main/java/org/oppia/app/view/ViewScope.kt | 6 + .../continue_button_answer_background.xml | 12 + .../main/res/drawable/ic_volume_off_48dp.xml | 4 + ...ty_numeric_input_interaction_view_test.xml | 110 +-- .../main/res/layout/add_profile_fragment.xml | 7 +- .../main/res/layout/admin_auth_fragment.xml | 7 +- app/src/main/res/layout/audio_fragment.xml | 22 +- .../layout/audio_fragment_test_activity.xml | 3 +- .../main/res/layout/cellular_data_dialog.xml | 7 +- .../res/layout/concept_card_example_view.xml | 9 +- .../concept_card_fragment_test_activity.xml | 5 +- app/src/main/res/layout/content_item.xml | 5 +- .../res/layout/continue_interaction_item.xml | 49 ++ .../main/res/layout/exploration_activity.xml | 43 +- .../main/res/layout/exploration_fragment.xml | 3 +- app/src/main/res/layout/feedback_item.xml | 27 + .../res/layout/fraction_interaction_item.xml | 44 ++ app/src/main/res/layout/home_fragment.xml | 3 +- .../item_selection_interaction_items.xml | 24 +- .../multiple_choice_interaction_items.xml | 12 +- .../layout/numeric_input_interaction_item.xml | 36 + app/src/main/res/layout/profile_activity.xml | 3 +- .../res/layout/profile_chooser_fragment.xml | 10 +- .../res/layout/question_player_activity.xml | 3 +- .../res/layout/question_player_fragment.xml | 7 +- .../res/layout/selection_interaction_item.xml | 22 +- app/src/main/res/layout/splash_activity.xml | 6 +- app/src/main/res/layout/state_button_item.xml | 27 +- app/src/main/res/layout/state_fragment.xml | 45 +- .../layout/state_fragment_test_activity.xml | 3 +- app/src/main/res/layout/story_activity.xml | 3 +- .../main/res/layout/story_chapter_view.xml | 6 +- app/src/main/res/layout/story_fragment.xml | 6 +- .../layout/story_fragment_test_activity.xml | 3 +- app/src/main/res/layout/story_header_view.xml | 5 +- app/src/main/res/layout/test_activity.xml | 6 +- app/src/main/res/layout/test_fragment.xml | 14 +- .../res/layout/test_html_parser_activity.xml | 6 +- ...test_text_view_for_int_no_data_binding.xml | 8 +- ...st_text_view_for_int_with_data_binding.xml | 14 +- ...t_text_view_for_string_no_data_binding.xml | 8 +- ...text_view_for_string_with_data_binding.xml | 28 +- .../res/layout/test_url_parser_activity.xml | 6 +- .../layout/text_input_interaction_item.xml | 35 + app/src/main/res/layout/topic_activity.xml | 3 +- .../res/layout/topic_overview_fragment.xml | 36 +- .../main/res/layout/topic_play_fragment.xml | 2 + .../main/res/layout/topic_review_fragment.xml | 7 +- .../res/layout/topic_review_summary_view.xml | 17 +- .../main/res/layout/topic_summary_view.xml | 14 +- .../main/res/layout/topic_train_fragment.xml | 22 +- .../res/layout/topic_train_skill_view.xml | 11 +- app/src/main/res/values/strings.xml | 7 +- .../org/oppia/app/home/HomeActivityTest.kt | 16 +- .../org/oppia/app/parser/HtmlParserTest.kt | 1 - .../app/player/audio/AudioFragmentTest.kt | 4 +- .../audio/CellularDataDialogFragmentTest.kt | 4 +- .../exploration/ExplorationActivityTest.kt | 17 +- .../app/player/state/StateFragmentTest.kt | 61 +- .../app/recyclerview/BindableAdapterTest.kt | 6 +- .../InputInteractionViewTestActivityTest.kt | 162 ++--- .../conceptcard/ConceptCardFragmentTest.kt | 2 +- .../app/topic/train/TopicTrainFragmentTest.kt | 24 +- .../org/oppia/app/utility/DrawableMatcher.kt | 6 +- .../app/utility/OrientationChangeAction.kt | 10 +- .../java/org/oppia/app/utility/TabMatcher.kt | 5 +- build.gradle | 1 + domain/build.gradle | 1 + .../main/assets/prototype_exploration.json | 630 ++++++++++++++++++ domain/src/main/assets/welcome.json | 2 +- .../domain/audio/AudioPlayerController.kt | 3 - .../AnswerClassificationController.kt | 1 - ...artExactlyEqualToRuleClassifierProvider.kt | 2 +- ...AndInSimplestFormRuleClassifierProvider.kt | 2 +- ...putIsEquivalentToRuleClassifierProvider.kt | 2 +- ...tIsExactlyEqualToRuleClassifierProvider.kt | 2 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 2 +- ...onInputIsLessThanRuleClassifierProvider.kt | 2 +- ...tainsAtLeastOneOfRuleClassifierProvider.kt | 2 +- ...ntainAtLeastOneOfRuleClassifierProvider.kt | 2 +- ...ectionInputEqualsRuleClassifierProvider.kt | 2 +- ...tIsProperSubsetOfRuleClassifierProvider.kt | 2 +- ...ChoiceInputEqualsRuleClassifierProvider.kt | 2 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 4 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 6 +- ...umericInputEqualsRuleClassifierProvider.kt | 2 +- ...aterThanOrEqualToRuleClassifierProvider.kt | 2 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 2 +- ...LessThanOrEqualToRuleClassifierProvider.kt | 2 +- ...icInputIsLessThanRuleClassifierProvider.kt | 2 +- ...seSensitiveEqualsRuleClassifierProvider.kt | 2 +- ...TextInputContainsRuleClassifierProvider.kt | 2 +- .../TextInputEqualsRuleClassifierProvider.kt | 2 +- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 2 +- ...xtInputStartsWithRuleClassifierProvider.kt | 2 +- .../exploration/ExplorationRetriever.kt | 3 +- .../domain/topic/StoryProgressController.kt | 5 +- .../org/oppia/domain/topic/TopicController.kt | 12 +- .../oppia/domain/topic/TopicListController.kt | 6 +- .../util/InteractionObjectExtensions.kt | 70 ++ .../oppia/domain/util/JsonAssetRetriever.kt | 1 - .../org/oppia/domain/util/StateRetriever.kt | 78 ++- .../domain/audio/AudioPlayerControllerTest.kt | 19 +- .../audio/CellularDialogControllerTest.kt | 2 +- .../AnswerClassificationControllerTest.kt | 4 +- .../ProfileManagementControllerTest.kt | 3 - model/src/main/proto/interaction_object.proto | 2 +- .../java/org/oppia/util/data/DataProviders.kt | 1 - .../java/org/oppia/util/logging/Logger.kt | 2 +- .../parser/ExplorationHtmlParserEntityType.kt | 0 .../org/oppia/util/parser/GlideImageLoader.kt | 3 - .../util/parser/GlideImageLoaderModule.kt | 2 + .../java/org/oppia/util/parser/HtmlParser.kt | 1 - .../util/parser/HtmlParserEntityTypeModule.kt | 0 .../java/org/oppia/util/parser/ImageLoader.kt | 5 - .../org/oppia/util/parser/UrlImageParser.kt | 12 +- .../util/profile/DirectoryManagementUtil.kt | 2 +- .../org/oppia/util/data/AsyncResultTest.kt | 2 - .../util/data/InMemoryBlockingCacheTest.kt | 2 - .../profile/DirectoryManagementUtilTest.kt | 12 - 195 files changed, 2570 insertions(+), 1527 deletions(-) delete mode 100644 app/src/main/java/org/oppia/app/customview/interaction/InteractionAnswerRetriever.kt create mode 100644 app/src/main/java/org/oppia/app/fragment/FragmentModule.kt delete mode 100755 app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt delete mode 100755 app/src/main/java/org/oppia/app/player/state/SelectInputItemsListener.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/SelectionInteractionView.kt delete mode 100755 app/src/main/java/org/oppia/app/player/state/StateAdapter.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt delete mode 100755 app/src/main/java/org/oppia/app/player/state/customview/SelectionInputInteractionView.kt mode change 100755 => 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/FeedbackViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/NumericInputViewModel.kt mode change 100755 => 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt delete mode 100755 app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionCustomizationArgsViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt delete mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateButtonViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateItemViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateNavigationButtonViewModel.kt create mode 100644 app/src/main/java/org/oppia/app/player/state/itemviewmodel/TextInputViewModel.kt delete mode 100644 app/src/main/java/org/oppia/app/player/state/listener/InputInteractionListener.kt delete mode 100644 app/src/main/java/org/oppia/app/player/state/listener/InteractionAnswerRetriever.kt delete mode 100755 app/src/main/java/org/oppia/app/player/state/listener/ItemClickListener.kt rename app/src/main/java/org/oppia/app/player/state/listener/{ButtonInteractionListener.kt => StateNavigationButtonListener.kt} (55%) create mode 100644 app/src/main/java/org/oppia/app/view/ViewComponent.kt create mode 100644 app/src/main/java/org/oppia/app/view/ViewScope.kt create mode 100644 app/src/main/res/drawable/continue_button_answer_background.xml create mode 100644 app/src/main/res/drawable/ic_volume_off_48dp.xml mode change 100755 => 100644 app/src/main/res/layout/content_item.xml create mode 100644 app/src/main/res/layout/continue_interaction_item.xml create mode 100644 app/src/main/res/layout/feedback_item.xml create mode 100644 app/src/main/res/layout/fraction_interaction_item.xml create mode 100644 app/src/main/res/layout/numeric_input_interaction_item.xml mode change 100755 => 100644 app/src/main/res/layout/selection_interaction_item.xml create mode 100644 app/src/main/res/layout/text_input_interaction_item.xml create mode 100644 domain/src/main/assets/prototype_exploration.json create mode 100644 domain/src/main/java/org/oppia/domain/util/InteractionObjectExtensions.kt mode change 100755 => 100644 domain/src/main/java/org/oppia/domain/util/StateRetriever.kt mode change 100755 => 100644 utility/src/main/java/org/oppia/util/parser/ExplorationHtmlParserEntityType.kt mode change 100755 => 100644 utility/src/main/java/org/oppia/util/parser/HtmlParserEntityTypeModule.kt diff --git a/app/build.gradle b/app/build.gradle index e4798b22142..69702f1ccb1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,6 +62,7 @@ dependencies { 'androidx.appcompat:appcompat:1.0.2', 'androidx.constraintlayout:constraintlayout:1.1.3', 'androidx.core:core-ktx:1.0.2', + "androidx.fragment:fragment:$fragment_version", 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03', 'androidx.multidex:multidex:2.0.1', 'androidx.recyclerview:recyclerview:1.0.0', @@ -72,6 +73,7 @@ dependencies { "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.mockito:mockito-core:2.7.22', ) testImplementation( 'androidx.test:core:1.2.0', @@ -82,6 +84,7 @@ dependencies { 'com.google.truth:truth:0.43', 'org.robolectric:robolectric:4.3', 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2', + 'org.mockito:mockito-core:2.7.22', ) androidTestImplementation( 'androidx.test:core:1.2.0', @@ -91,7 +94,8 @@ dependencies { 'androidx.test.ext:junit:1.1.1', 'androidx.test:runner:1.2.0', 'com.google.truth:truth:0.43', - 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2' + 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2', + 'org.mockito:mockito-android:2.7.22', ) androidTestUtil( 'androidx.test:orchestrator:1.2.0', diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3be33405c45..ef846c9da5a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,9 @@ - + + fun inject(addProfileFragment: AddProfileFragment) fun inject(adminAuthFragment: AdminAuthFragment) fun inject(audioFragment: AudioFragment) diff --git a/app/src/main/java/org/oppia/app/fragment/FragmentModule.kt b/app/src/main/java/org/oppia/app/fragment/FragmentModule.kt new file mode 100644 index 00000000000..3592fc0d8f7 --- /dev/null +++ b/app/src/main/java/org/oppia/app/fragment/FragmentModule.kt @@ -0,0 +1,7 @@ +package org.oppia.app.fragment + +import dagger.Module +import org.oppia.app.view.ViewComponent + +/** Root fragment module. */ +@Module(subcomponents = [ViewComponent::class]) class FragmentModule diff --git a/app/src/main/java/org/oppia/app/fragment/InjectableDialogFragment.kt b/app/src/main/java/org/oppia/app/fragment/InjectableDialogFragment.kt index 2f0f96f3dde..d51e5b26116 100644 --- a/app/src/main/java/org/oppia/app/fragment/InjectableDialogFragment.kt +++ b/app/src/main/java/org/oppia/app/fragment/InjectableDialogFragment.kt @@ -16,7 +16,7 @@ abstract class InjectableDialogFragment: DialogFragment() { */ lateinit var fragmentComponent: FragmentComponent - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent = (requireActivity() as InjectableAppCompatActivity).createFragmentComponent(this) } diff --git a/app/src/main/java/org/oppia/app/fragment/InjectableFragment.kt b/app/src/main/java/org/oppia/app/fragment/InjectableFragment.kt index 25c3628f07a..8504c3e9a46 100644 --- a/app/src/main/java/org/oppia/app/fragment/InjectableFragment.kt +++ b/app/src/main/java/org/oppia/app/fragment/InjectableFragment.kt @@ -1,8 +1,10 @@ package org.oppia.app.fragment import android.content.Context +import android.view.View import androidx.fragment.app.Fragment import org.oppia.app.activity.InjectableAppCompatActivity +import org.oppia.app.view.ViewComponent /** * A fragment that facilitates field injection to children. This fragment can only be used with @@ -16,8 +18,12 @@ abstract class InjectableFragment: Fragment() { */ lateinit var fragmentComponent: FragmentComponent - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent = (requireActivity() as InjectableAppCompatActivity).createFragmentComponent(this) } + + fun createViewComponent(view: View): ViewComponent { + return fragmentComponent.getViewComponentBuilderProvider().get().setView(view).build() + } } diff --git a/app/src/main/java/org/oppia/app/home/HomeFragment.kt b/app/src/main/java/org/oppia/app/home/HomeFragment.kt index d22e1524374..bcde5a35255 100644 --- a/app/src/main/java/org/oppia/app/home/HomeFragment.kt +++ b/app/src/main/java/org/oppia/app/home/HomeFragment.kt @@ -14,7 +14,7 @@ import javax.inject.Inject class HomeFragment : InjectableFragment(), TopicSummaryClickListener { @Inject lateinit var homeFragmentPresenter: HomeFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt b/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt index ada9daff3e2..2d52065f9f9 100644 --- a/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt @@ -20,14 +20,14 @@ import org.oppia.app.model.TopicSummary import org.oppia.app.model.UserAppHistory import org.oppia.domain.UserAppHistoryController import org.oppia.domain.exploration.ExplorationDataController -import org.oppia.domain.exploration.TEST_EXPLORATION_ID_5 +import org.oppia.domain.exploration.TEST_EXPLORATION_ID_30 import org.oppia.domain.topic.TEST_TOPIC_ID_0 import org.oppia.domain.topic.TopicListController import org.oppia.util.data.AsyncResult import org.oppia.util.logging.Logger import javax.inject.Inject -private const val EXPLORATION_ID = TEST_EXPLORATION_ID_5 +private const val EXPLORATION_ID = TEST_EXPLORATION_ID_30 private const val TAG_HOME_FRAGMENT = "HomeFragment" /** The presenter for [HomeFragment]. */ @@ -82,6 +82,7 @@ class HomeFragmentPresenter @Inject constructor( } fun playExplorationButton(v: View) { + explorationDataController.stopPlayingExploration() explorationDataController.startPlayingExploration( EXPLORATION_ID ).observe(fragment, Observer> { result -> diff --git a/app/src/main/java/org/oppia/app/home/continueplaying/ContinuePlayingFragment.kt b/app/src/main/java/org/oppia/app/home/continueplaying/ContinuePlayingFragment.kt index ab4043b69a2..3afc7a5129d 100644 --- a/app/src/main/java/org/oppia/app/home/continueplaying/ContinuePlayingFragment.kt +++ b/app/src/main/java/org/oppia/app/home/continueplaying/ContinuePlayingFragment.kt @@ -13,7 +13,7 @@ import javax.inject.Inject class ContinuePlayingFragment : InjectableFragment(), OngoingStoryClickListener { @Inject lateinit var continuePlayingFragmentPresenter: ContinuePlayingFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt b/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt index 0469191daa5..9a571d0266f 100755 --- a/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt +++ b/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt @@ -5,10 +5,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.databinding.ObservableField import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel -import org.oppia.app.home.continueplaying.ContinuePlayingActivity import org.oppia.app.home.HomeItemViewModel import org.oppia.app.home.RouteToContinuePlayingListener import org.oppia.app.home.RouteToTopicPlayStoryListener +import org.oppia.app.home.continueplaying.ContinuePlayingActivity import org.oppia.app.model.PromotedStory import org.oppia.app.topic.TopicActivity diff --git a/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt b/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt index a0be5207e55..722ec79a86b 100755 --- a/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt @@ -3,7 +3,6 @@ package org.oppia.app.home.topiclist import android.graphics.Color import android.view.View import androidx.annotation.ColorInt -import androidx.lifecycle.ViewModel import org.oppia.app.home.HomeItemViewModel import org.oppia.app.model.TopicSummary diff --git a/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt b/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt index ad60458c009..33d2facb17d 100644 --- a/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt +++ b/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt @@ -1,34 +1,56 @@ package org.oppia.app.parser import org.oppia.app.model.Fraction +import org.oppia.domain.util.normalizeWhitespace /** This class contains method that helps to parse string to fraction. */ class StringToFractionParser { + private val wholeNumberOnlyRegex = """^-? ?(\d+)$""".toRegex() + private val fractionOnlyRegex = """^-? ?(\d+) ?/ ?(\d)+$""".toRegex() + private val mixedNumberRegex = """^-? ?(\d)+ ?(\d+) ?/ ?(\d)+$""".toRegex() + fun getFractionFromString(text: String): Fraction { - var inputText: String = text - var isNegative = false - var numerator = "0" - var denominator = "0" - var wholeNumber = "0" - val fractionObjectBuilder = Fraction.newBuilder() - if (inputText.startsWith("-")) - isNegative = true - inputText = inputText.replace("-", "").trim() - wholeNumber = if (inputText.contains("/") && inputText.contains(" ")) { - inputText.substringBefore(" ") - } else if (inputText.contains("/")) { - wholeNumber - } else { - inputText - } - inputText = - if (inputText.contains(" ")) inputText.substringAfter(" ").replace(" ", "") else inputText.replace(" ", "") - if (inputText.contains("/")) { - numerator = inputText.substringBefore("/") - denominator = inputText.substringAfter("/") - } - fractionObjectBuilder.setIsNegative(isNegative).setNumerator(numerator.toInt()) - .setDenominator(denominator.toInt()).wholeNumber = wholeNumber.toInt() - return fractionObjectBuilder.build() + // Normalize whitespace to ensure that answer follows a simpler subset of possible patterns. + val inputText: String = text.normalizeWhitespace() + return parseMixedNumber(inputText) + ?: parseFraction(inputText) + ?: parseWholeNumber(inputText) + ?: throw IllegalArgumentException("Incorrectly formatted fraction: $text") + } + + private fun parseMixedNumber(inputText: String): Fraction? { + val mixedNumberMatch = mixedNumberRegex.matchEntire(inputText) ?: return null + val (_, wholeNumberText, numeratorText, denominatorText) = mixedNumberMatch.groupValues + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setWholeNumber(wholeNumberText.toInt()) + .setNumerator(numeratorText.toInt()) + .setDenominator(denominatorText.toInt()) + .build() + } + + private fun parseFraction(inputText: String): Fraction? { + val fractionOnlyMatch = fractionOnlyRegex.matchEntire(inputText) ?: return null + val (_, numeratorText, denominatorText) = fractionOnlyMatch.groupValues + // Fraction-only numbers imply no whole number. + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setNumerator(numeratorText.toInt()) + .setDenominator(denominatorText.toInt()) + .build() + } + + private fun parseWholeNumber(inputText: String): Fraction? { + val wholeNumberMatch = wholeNumberOnlyRegex.matchEntire(inputText) ?: return null + val (_, wholeNumberText) = wholeNumberMatch.groupValues + // Whole number fractions imply '0/1' fractional parts. + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setWholeNumber(wholeNumberText.toInt()) + .setNumerator(0) + .setDenominator(1) + .build() } + + private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") } diff --git a/app/src/main/java/org/oppia/app/player/audio/AudioFragment.kt b/app/src/main/java/org/oppia/app/player/audio/AudioFragment.kt index 50ebfab35e8..57328e23272 100755 --- a/app/src/main/java/org/oppia/app/player/audio/AudioFragment.kt +++ b/app/src/main/java/org/oppia/app/player/audio/AudioFragment.kt @@ -34,7 +34,7 @@ class AudioFragment : InjectableFragment(), LanguageInterface { @Inject lateinit var audioFragmentPresenter: AudioFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/player/audio/AudioFragmentPresenter.kt b/app/src/main/java/org/oppia/app/player/audio/AudioFragmentPresenter.kt index eb99183e4c6..aa9aded8043 100755 --- a/app/src/main/java/org/oppia/app/player/audio/AudioFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/player/audio/AudioFragmentPresenter.kt @@ -19,7 +19,6 @@ import org.oppia.domain.exploration.ExplorationDataController import org.oppia.util.data.AsyncResult import org.oppia.util.logging.Logger import javax.inject.Inject -import kotlin.collections.ArrayList private const val TAG_LANGUAGE_DIALOG = "LANGUAGE_DIALOG" private const val KEY_SELECTED_LANGUAGE = "SELECTED_LANGUAGE" diff --git a/app/src/main/java/org/oppia/app/player/audio/CellularDataDialogFragment.kt b/app/src/main/java/org/oppia/app/player/audio/CellularDataDialogFragment.kt index 417465ef018..187bd4ccbfe 100755 --- a/app/src/main/java/org/oppia/app/player/audio/CellularDataDialogFragment.kt +++ b/app/src/main/java/org/oppia/app/player/audio/CellularDataDialogFragment.kt @@ -3,9 +3,9 @@ package org.oppia.app.player.audio import android.app.Dialog import android.content.Context import android.os.Bundle +import android.widget.CheckBox import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment -import android.widget.CheckBox import org.oppia.app.R import org.oppia.app.player.state.StateFragment diff --git a/app/src/main/java/org/oppia/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/app/player/exploration/ExplorationActivityPresenter.kt index 94faaf4371c..9d4c2743892 100755 --- a/app/src/main/java/org/oppia/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/app/player/exploration/ExplorationActivityPresenter.kt @@ -1,6 +1,7 @@ package org.oppia.app.player.exploration import android.os.Bundle +import android.view.View import androidx.appcompat.app.AppCompatActivity import org.oppia.app.R import org.oppia.app.activity.ActivityScope @@ -11,6 +12,9 @@ import javax.inject.Inject class ExplorationActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { fun handleOnCreate(explorationId: String) { activity.setContentView(R.layout.exploration_activity) + + activity.setSupportActionBar(activity.findViewById(R.id.exploration_toolbar)) + if (getExplorationFragment() == null) { val explorationFragment = ExplorationFragment() val args = Bundle() @@ -21,9 +25,15 @@ class ExplorationActivityPresenter @Inject constructor(private val activity: App explorationFragment ).commitNow() } + + activity.findViewById(R.id.enable_audio_playback_button).setOnClickListener { + getExplorationFragment()?.handlePlayAudio() + } } private fun getExplorationFragment(): ExplorationFragment? { - return activity.supportFragmentManager.findFragmentById(R.id.exploration_fragment_placeholder) as ExplorationFragment? + return activity.supportFragmentManager.findFragmentById( + R.id.exploration_fragment_placeholder + ) as ExplorationFragment? } } diff --git a/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragment.kt b/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragment.kt index 337112eee6c..998a5bd52ba 100755 --- a/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragment.kt +++ b/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragment.kt @@ -12,7 +12,7 @@ import javax.inject.Inject class ExplorationFragment : InjectableFragment() { @Inject lateinit var explorationFragmentPresenter: ExplorationFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } @@ -20,4 +20,6 @@ class ExplorationFragment : InjectableFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return explorationFragmentPresenter.handleCreateView(inflater, container) } + + fun handlePlayAudio() = explorationFragmentPresenter.handlePlayAudio() } diff --git a/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragmentPresenter.kt b/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragmentPresenter.kt index 4b7dd45ed76..e3bea8ae910 100755 --- a/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/player/exploration/ExplorationFragmentPresenter.kt @@ -1,6 +1,5 @@ package org.oppia.app.player.exploration -import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -31,6 +30,10 @@ class ExplorationFragmentPresenter @Inject constructor( return binding } + fun handlePlayAudio() { + getStateFragment()?.handlePlayAudio() + } + private fun getStateFragment(): StateFragment? { return fragment.childFragmentManager.findFragmentById(R.id.state_fragment_placeholder) as StateFragment? } diff --git a/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt b/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt deleted file mode 100755 index ac2c677d447..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt +++ /dev/null @@ -1,200 +0,0 @@ -package org.oppia.app.player.state - -import android.text.Spannable -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.databinding.library.baseAdapters.BR -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.item_selection_interaction_items.view.item_selection_checkbox -import kotlinx.android.synthetic.main.item_selection_interaction_items.view.item_selection_contents_text_view -import kotlinx.android.synthetic.main.multiple_choice_interaction_items.view.multiple_choice_content_text_view -import kotlinx.android.synthetic.main.multiple_choice_interaction_items.view.multiple_choice_radio_button -import org.oppia.app.R -import org.oppia.app.databinding.ItemSelectionInteractionItemsBinding -import org.oppia.app.databinding.MultipleChoiceInteractionItemsBinding -import org.oppia.app.model.InteractionObject -import org.oppia.app.model.StringList -import org.oppia.app.player.state.itemviewmodel.SelectionInteractionContentViewModel -import org.oppia.app.player.state.itemviewmodel.SelectionInteractionCustomizationArgsViewModel -import org.oppia.app.player.state.listener.ItemClickListener -import org.oppia.util.logging.Logger -import org.oppia.util.parser.HtmlParser - -private const val VIEW_TYPE_RADIO_BUTTONS = 1 -private const val VIEW_TYPE_CHECKBOXES = 2 -private const val INTERACTION_ADAPTER_TAG = "Interaction Adapter" - -/** - * Adapter to bind the interactions to the [RecyclerView]. It handles MultipleChoiceInput - * and ItemSelectionInput interaction views. - */ -class InteractionAdapter( - private val logger: Logger, - private val htmlParserFactory: HtmlParser.Factory, - private val entityType: String, - private val explorationId: String, - private val itemList: MutableList, - private val selectionInteractionCustomizationArgsViewModel: SelectionInteractionCustomizationArgsViewModel, - private val itemClickListener: ItemClickListener, - private val selectedInputItemIndexes: ArrayList, - private val selectInputItemsListener: SelectInputItemsListener -) : RecyclerView.Adapter() { - - private var itemSelectedPosition = -1 - private var selectedAnswerIndex = -1 - private var selectedHtmlStringList = mutableListOf() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - VIEW_TYPE_RADIO_BUTTONS -> { - val inflater = LayoutInflater.from(parent.context) - val binding = - DataBindingUtil.inflate( - inflater, - R.layout.multiple_choice_interaction_items, - parent, - /* attachToParent= */ false - ) - MultipleChoiceViewHolder(binding) - } - VIEW_TYPE_CHECKBOXES -> { - val inflater = LayoutInflater.from(parent.context) - val binding = - DataBindingUtil.inflate( - inflater, - R.layout.item_selection_interaction_items, - parent, - /* attachToParent= */ false - ) - ItemSelectionViewHolder(binding) - } - else -> throw IllegalArgumentException("Invalid view type: $viewType") - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder.itemViewType) { - VIEW_TYPE_RADIO_BUTTONS -> (holder as MultipleChoiceViewHolder).bind( - itemList[position].htmlContent, - position, - itemSelectedPosition - ) - VIEW_TYPE_CHECKBOXES -> (holder as ItemSelectionViewHolder).bind( - itemList[position], - position - ) - } - } - - // Determines the appropriate ViewType according to the interaction type. - override fun getItemViewType(position: Int): Int { - return if (selectionInteractionCustomizationArgsViewModel.interactionId == "ItemSelectionInput") { - if (selectionInteractionCustomizationArgsViewModel.maxAllowableSelectionCount > 1) { - VIEW_TYPE_CHECKBOXES - } else { - VIEW_TYPE_RADIO_BUTTONS - } - } else { - VIEW_TYPE_RADIO_BUTTONS - } - } - - override fun getItemCount(): Int { - return itemList.size - } - - inner class ItemSelectionViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { - internal fun bind(selectionInteractionContentViewModel: SelectionInteractionContentViewModel, position: Int) { - - var isOptionSelected = false - - if (selectedInputItemIndexes.contains(position) || selectionInteractionContentViewModel.isAnswerSelected) { - isOptionSelected = true - } - - binding.setVariable(BR.htmlContent, selectionInteractionContentViewModel.htmlContent) - binding.executePendingBindings() - val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( - selectionInteractionContentViewModel.htmlContent, - binding.root.item_selection_contents_text_view - ) - binding.root.item_selection_contents_text_view.text = htmlResult - binding.root.item_selection_checkbox.isChecked = isOptionSelected - binding.root.setOnClickListener { - if (binding.root.item_selection_checkbox.isChecked) { - if (!selectedInputItemIndexes.contains(position)) { - selectedInputItemIndexes.add(position) - } else { - selectedInputItemIndexes.remove(position) - } - selectInputItemsListener.onInputItemSelection(selectedInputItemIndexes) - itemList[adapterPosition].isAnswerSelected = false - selectedHtmlStringList.remove(binding.root.item_selection_contents_text_view.text.toString()) - } else { - if (selectedHtmlStringList.size != selectionInteractionCustomizationArgsViewModel.maxAllowableSelectionCount) { - itemList[adapterPosition].isAnswerSelected = true - selectedHtmlStringList.add(binding.root.item_selection_contents_text_view.text.toString()) - if (selectedInputItemIndexes.contains(position)) { - selectedInputItemIndexes.remove(position) - } else { - selectedInputItemIndexes.add(position) - } - } else { - logger.d( - INTERACTION_ADAPTER_TAG, - "You cannot select more than ${selectionInteractionCustomizationArgsViewModel.maxAllowableSelectionCount} options" - ) - } - } - notifyDataSetChanged() - val interactionObjectBuilder = InteractionObject.newBuilder() - if (selectedHtmlStringList.size >= 0) { - interactionObjectBuilder.setOfHtmlString = StringList.newBuilder().addAllHtml(selectedHtmlStringList).build() - } else { - if (selectedAnswerIndex >= 0) { - interactionObjectBuilder.nonNegativeInt = selectedAnswerIndex - } - } - selectInputItemsListener.onInputItemSelection(selectedInputItemIndexes) - itemClickListener.onItemClick(interactionObjectBuilder.build()) - } - } - } - - inner class MultipleChoiceViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { - internal fun bind(rawString: String, position: Int, selectedPosition: Int) { - - var isOptionSelected = false - - if (selectedInputItemIndexes.contains(position) || selectedPosition == position) { - isOptionSelected = true - } - - binding.setVariable(BR.htmlContent, rawString) - binding.executePendingBindings() - val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( - rawString, - binding.root.multiple_choice_content_text_view - ) - binding.root.multiple_choice_content_text_view.text = htmlResult - binding.root.multiple_choice_radio_button.isChecked = isOptionSelected - binding.root.setOnClickListener { - - selectedInputItemIndexes.clear() - selectedInputItemIndexes.add(position) - selectInputItemsListener.onInputItemSelection(selectedInputItemIndexes) - - itemSelectedPosition = adapterPosition - selectedAnswerIndex = adapterPosition - notifyDataSetChanged() - val interactionObjectBuilder = InteractionObject.newBuilder() - if (selectedAnswerIndex >= 0) { - interactionObjectBuilder.nonNegativeInt = selectedAnswerIndex - } - itemClickListener.onItemClick(interactionObjectBuilder.build()) - } - } - } -} diff --git a/app/src/main/java/org/oppia/app/player/state/SelectInputItemsListener.kt b/app/src/main/java/org/oppia/app/player/state/SelectInputItemsListener.kt deleted file mode 100755 index 12d95e8f84d..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/SelectInputItemsListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.oppia.app.player.state - -/** - * Interface to keep track of selected options in MultipleChoiceInput and ItemSelectionInput. - */ -interface SelectInputItemsListener { - fun onInputItemSelection(indexList: ArrayList) -} diff --git a/app/src/main/java/org/oppia/app/player/state/SelectionInteractionView.kt b/app/src/main/java/org/oppia/app/player/state/SelectionInteractionView.kt new file mode 100644 index 00000000000..cf4d2d9eafd --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/SelectionInteractionView.kt @@ -0,0 +1,112 @@ +package org.oppia.app.player.state + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.databinding.BindingAdapter +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import org.oppia.app.databinding.ItemSelectionInteractionItemsBinding +import org.oppia.app.databinding.MultipleChoiceInteractionItemsBinding +import org.oppia.app.fragment.InjectableFragment +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionContentViewModel +import org.oppia.app.recyclerview.BindableAdapter +import org.oppia.util.parser.ExplorationHtmlParserEntityType +import org.oppia.util.parser.HtmlParser +import javax.inject.Inject + +/** Corresponds to the type of input that should be used for an item selection interaction view. */ +enum class SelectionItemInputType { + CHECKBOXES, + RADIO_BUTTONS +} + +/** + * A custom [RecyclerView] for displaying a variable list of items that may be selected by a user as part of the item + * selection or multiple choice interactions. + */ +class SelectionInteractionView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr) { + // Default to checkboxes to ensure that something can render even if it may not be correct. + private var selectionItemInputType: SelectionItemInputType = SelectionItemInputType.CHECKBOXES + + @Inject lateinit var htmlParserFactory: HtmlParser.Factory + @Inject @field:ExplorationHtmlParserEntityType lateinit var entityType: String + private lateinit var explorationId: String + + init { + adapter = createAdapter() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + FragmentManager.findFragment(this).createViewComponent(this).inject(this) + } + + fun setItemInputType(selectionItemInputType: SelectionItemInputType) { + // TODO(#299): Find a cleaner way to initialize the item input type. Using data-binding results in a race condition + // with setting the adapter data, so this needs to be done in an order-agnostic way. There should be a way to do + // this more efficiently and cleanly than always relying on notifying of potential changes in the adapter when the + // type is set (plus the type ought to be permanent). + this.selectionItemInputType = selectionItemInputType + adapter!!.notifyDataSetChanged() + } + + // TODO(#264): Clean up HTML parser such that it can be handled completely through a binding adapter, allowing + // TextViews that require custom Oppia HTML parsing to be fully automatically bound through data-binding. + fun setExplorationId(explorationId: String) { + this.explorationId = explorationId + } + + private fun createAdapter(): BindableAdapter { + return BindableAdapter.Builder + .newBuilder() + .registerViewTypeComputer { selectionItemInputType.ordinal } + .registerViewBinder( + viewType = SelectionItemInputType.CHECKBOXES.ordinal, + inflateView = { parent -> + ItemSelectionInteractionItemsBinding.inflate( + LayoutInflater.from(parent.context), parent, /* attachToParent= */ false + ).root + }, + bindView = { view, viewModel -> + val binding = DataBindingUtil.findBinding(view)!! + binding.htmlContent = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( + viewModel.htmlContent, binding.itemSelectionContentsTextView + ) + binding.viewModel = viewModel + } + ) + .registerViewBinder( + viewType = SelectionItemInputType.RADIO_BUTTONS.ordinal, + inflateView = { parent -> + MultipleChoiceInteractionItemsBinding.inflate( + LayoutInflater.from(parent.context), parent, /* attachToParent= */ false + ).root + }, + bindView = { view, viewModel -> + val binding = DataBindingUtil.findBinding(view)!! + binding.htmlContent = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( + viewModel.htmlContent, binding.multipleChoiceContentTextView + ) + binding.viewModel = viewModel + } + ) + .build() + } +} + +/** Sets the [SelectionItemInputType] for a specific [SelectionInteractionView] via data-binding. */ +@BindingAdapter("itemInputType") +fun setItemInputType( + selectionInteractionView: SelectionInteractionView, selectionItemInputType: SelectionItemInputType +) = selectionInteractionView.setItemInputType(selectionItemInputType) + + +/** Sets the exploration ID for a specific [SelectionInteractionView] via data-binding. */ +@BindingAdapter("explorationId") +fun setExplorationId( + selectionInteractionView: SelectionInteractionView, explorationId: String +) = selectionInteractionView.setExplorationId(explorationId) diff --git a/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt b/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt deleted file mode 100755 index a01a1572062..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt +++ /dev/null @@ -1,189 +0,0 @@ -package org.oppia.app.player.state - -import android.text.Spannable -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.recyclerview.widget.RecyclerView -import androidx.databinding.library.baseAdapters.BR -import kotlinx.android.synthetic.main.content_item.view.content_text_view -import kotlinx.android.synthetic.main.selection_interaction_item.view.selection_interaction_frameLayout -import kotlinx.android.synthetic.main.state_button_item.view.* -import org.oppia.app.R -import org.oppia.app.databinding.ContentItemBinding -import org.oppia.app.databinding.SelectionInteractionItemBinding -import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel -import org.oppia.app.player.state.listener.ButtonInteractionListener -import org.oppia.app.databinding.StateButtonItemBinding -import org.oppia.app.model.InteractionObject -import org.oppia.app.player.state.itemviewmodel.ContentViewModel -import org.oppia.app.player.state.itemviewmodel.SelectionInteractionCustomizationArgsViewModel -import org.oppia.app.player.state.itemviewmodel.SelectionInteractionContentViewModel -import org.oppia.app.player.state.listener.ItemClickListener -import org.oppia.util.logging.Logger -import org.oppia.util.parser.HtmlParser - -@Suppress("unused") -private const val VIEW_TYPE_CONTENT = 1 -@Suppress("unused") -private const val VIEW_TYPE_INTERACTION_READ_ONLY = 2 -@Suppress("unused") -private const val VIEW_TYPE_NUMERIC_INPUT_INTERACTION = 3 -@Suppress("unused") -private const val VIEW_TYPE_TEXT_INPUT_INTERACTION = 4 -private const val VIEW_TYPE_STATE_BUTTON = 5 -const val VIEW_TYPE_SELECTION_INTERACTION = 6 - -/** Adapter to inflate different items/views inside [RecyclerView]. The itemList consists of various ViewModels. */ -class StateAdapter( - private val logger: Logger, - private val itemList: MutableList, - private val buttonInteractionListener: ButtonInteractionListener, - private val htmlParserFactory: HtmlParser.Factory, - private val entityType: String, - private val explorationId: String, - private val selectedInputItemIndexes: ArrayList, - private val selectInputItemsListener: SelectInputItemsListener -) : - RecyclerView.Adapter() { - - lateinit var stateButtonViewModel: StateButtonViewModel - private var interactionObjectBuilder: InteractionObject = InteractionObject.newBuilder().build() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - // TODO(#249): Generalize this binding to make adding future interactions easier. - VIEW_TYPE_STATE_BUTTON -> { - val inflater = LayoutInflater.from(parent.context) - val binding = - DataBindingUtil.inflate( - inflater, - R.layout.state_button_item, - parent, - /* attachToParent= */false - ) - StateButtonViewHolder(binding, buttonInteractionListener) - } - VIEW_TYPE_CONTENT -> { - val inflater = LayoutInflater.from(parent.context) - val binding = - DataBindingUtil.inflate( - inflater, - R.layout.content_item, - parent, - /* attachToParent= */ false - ) - ContentViewHolder(binding) - } - VIEW_TYPE_SELECTION_INTERACTION -> { - val inflater = LayoutInflater.from(parent.context) - val binding = - DataBindingUtil.inflate( - inflater, - R.layout.selection_interaction_item, - parent, - /* attachToParent= */ false - ) - SelectionInteractionViewHolder(binding) - } - else -> throw IllegalArgumentException("Invalid view type: $viewType") - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder.itemViewType) { - VIEW_TYPE_STATE_BUTTON -> { - (holder as StateButtonViewHolder).bind(itemList[position] as StateButtonViewModel) - } - VIEW_TYPE_CONTENT -> { - (holder as ContentViewHolder).bind((itemList[position] as ContentViewModel).htmlContent) - } - VIEW_TYPE_SELECTION_INTERACTION -> { - (holder as SelectionInteractionViewHolder).bind(itemList[position] as SelectionInteractionCustomizationArgsViewModel) - } - } - } - - override fun getItemViewType(position: Int): Int { - return when (itemList[position]) { - is ContentViewModel -> VIEW_TYPE_CONTENT - is SelectionInteractionCustomizationArgsViewModel -> VIEW_TYPE_SELECTION_INTERACTION - is StateButtonViewModel -> { - stateButtonViewModel = itemList[position] as StateButtonViewModel - VIEW_TYPE_STATE_BUTTON - } - else -> throw IllegalArgumentException("Invalid type of data $position: ${itemList[position]}") - } - } - - override fun getItemCount(): Int { - return itemList.size - } - - inner class ContentViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { - internal fun bind(rawString: String) { - binding.setVariable(BR.htmlContent, rawString) - binding.executePendingBindings() - val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( - rawString, - binding.root.content_text_view - ) - binding.root.content_text_view.text = htmlResult - } - } - - private class StateButtonViewHolder( - val binding: ViewDataBinding, - private val buttonInteractionListener: ButtonInteractionListener - ) : RecyclerView.ViewHolder(binding.root) { - internal fun bind(stateButtonViewModel: StateButtonViewModel) { - binding.setVariable(BR.buttonViewModel, stateButtonViewModel) - binding.root.interaction_button.setOnClickListener { - buttonInteractionListener.onInteractionButtonClicked() - } - binding.root.next_state_image_view.setOnClickListener { - buttonInteractionListener.onNextButtonClicked() - } - binding.root.previous_state_image_view.setOnClickListener { - buttonInteractionListener.onPreviousButtonClicked() - } - binding.executePendingBindings() - } - } - - inner class SelectionInteractionViewHolder( - private val binding: ViewDataBinding - ) : RecyclerView.ViewHolder(binding.root), ItemClickListener { - - override fun onItemClick(interactionObject: InteractionObject) { - interactionObjectBuilder = interactionObject - } - - internal fun bind(customizationArgs: SelectionInteractionCustomizationArgsViewModel) { - val items: Array? - val choiceInteractionContentList: MutableList = ArrayList() - val gaeCustomArgsInString = customizationArgs.choiceItems.toString().replace("[", "").replace("]", "") - items = gaeCustomArgsInString.split(",").toTypedArray() - for (values in items) { - val selectionContentViewModel = SelectionInteractionContentViewModel() - selectionContentViewModel.htmlContent = values - selectionContentViewModel.isAnswerSelected = false - choiceInteractionContentList.add(selectionContentViewModel) - } - val interactionAdapter = - InteractionAdapter( - logger, - htmlParserFactory, - entityType, - explorationId, - choiceInteractionContentList, - customizationArgs, - this as ItemClickListener, - selectedInputItemIndexes, - selectInputItemsListener - ) - binding.root.selection_interaction_frameLayout.setAdapter(interactionAdapter) - } - } -} diff --git a/app/src/main/java/org/oppia/app/player/state/StateFragment.kt b/app/src/main/java/org/oppia/app/player/state/StateFragment.kt index 6e96bd837d1..5455b216797 100755 --- a/app/src/main/java/org/oppia/app/player/state/StateFragment.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateFragment.kt @@ -6,13 +6,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import org.oppia.app.fragment.InjectableFragment +import org.oppia.app.model.InteractionObject import org.oppia.app.player.audio.CellularDataInterface +import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver import javax.inject.Inject -internal const val KEY_SELECTED_INPUT_INDEXES = "SELECTED_INPUT_INDEXES" - /** Fragment that represents the current state of an exploration. */ -class StateFragment : InjectableFragment(), CellularDataInterface, SelectInputItemsListener { +class StateFragment : InjectableFragment(), CellularDataInterface, InteractionAnswerReceiver { companion object { /** * Creates a new instance of a StateFragment. @@ -28,37 +28,29 @@ class StateFragment : InjectableFragment(), CellularDataInterface, SelectInputIt } } - private var selectedInputItemIndexes = ArrayList() - @Inject lateinit var stateFragmentPresenter: StateFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - if (savedInstanceState != null) { - selectedInputItemIndexes = savedInstanceState.getIntegerArrayList(KEY_SELECTED_INPUT_INDEXES) - } - return stateFragmentPresenter.handleCreateView(inflater, container, selectedInputItemIndexes, this as SelectInputItemsListener) + return stateFragmentPresenter.handleCreateView(inflater, container) } - override fun enableAudioWhileOnCellular(saveUserChoice: Boolean) = + override fun enableAudioWhileOnCellular(saveUserChoice: Boolean) { stateFragmentPresenter.handleEnableAudio(saveUserChoice) + } - override fun disableAudioWhileOnCellular(saveUserChoice: Boolean) = + override fun disableAudioWhileOnCellular(saveUserChoice: Boolean) { stateFragmentPresenter.handleDisableAudio(saveUserChoice) - - fun dummyButtonClicked() = stateFragmentPresenter.handleAudioClick() - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putIntegerArrayList(KEY_SELECTED_INPUT_INDEXES, selectedInputItemIndexes) } - override fun onInputItemSelection(indexList: ArrayList) { - selectedInputItemIndexes = indexList + override fun onAnswerReadyForSubmission(answer: InteractionObject) { + stateFragmentPresenter.handleAnswerReadyForSubmission(answer) } + + fun handlePlayAudio() = stateFragmentPresenter.handleAudioClick() } diff --git a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt index 51067aee65c..9e2fb0101b7 100755 --- a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt @@ -6,25 +6,46 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.Transformations +import androidx.recyclerview.widget.RecyclerView import org.oppia.app.R +import org.oppia.app.databinding.ContentItemBinding +import org.oppia.app.databinding.ContinueInteractionItemBinding +import org.oppia.app.databinding.FeedbackItemBinding +import org.oppia.app.databinding.FractionInteractionItemBinding +import org.oppia.app.databinding.NumericInputInteractionItemBinding +import org.oppia.app.databinding.SelectionInteractionItemBinding +import org.oppia.app.databinding.StateButtonItemBinding import org.oppia.app.databinding.StateFragmentBinding +import org.oppia.app.databinding.TextInputInteractionItemBinding import org.oppia.app.fragment.FragmentScope +import org.oppia.app.model.AnswerAndResponse import org.oppia.app.model.AnswerOutcome import org.oppia.app.model.CellularDataPreference import org.oppia.app.model.EphemeralState +import org.oppia.app.model.Interaction import org.oppia.app.model.InteractionObject import org.oppia.app.model.SubtitledHtml import org.oppia.app.player.audio.AudioFragment import org.oppia.app.player.audio.CellularDataDialogFragment -import org.oppia.app.player.exploration.ExplorationActivity +import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.app.player.state.itemviewmodel.ContentViewModel -import org.oppia.app.player.state.itemviewmodel.SelectionInteractionCustomizationArgsViewModel -import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel -import org.oppia.app.player.state.listener.ButtonInteractionListener +import org.oppia.app.player.state.itemviewmodel.ContinueInteractionViewModel +import org.oppia.app.player.state.itemviewmodel.FeedbackViewModel +import org.oppia.app.player.state.itemviewmodel.FractionInteractionViewModel +import org.oppia.app.player.state.itemviewmodel.InteractionViewModelFactory +import org.oppia.app.player.state.itemviewmodel.NumericInputViewModel +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionViewModel +import org.oppia.app.player.state.itemviewmodel.StateItemViewModel +import org.oppia.app.player.state.itemviewmodel.StateNavigationButtonViewModel +import org.oppia.app.player.state.itemviewmodel.StateNavigationButtonViewModel.ContinuationNavigationButtonType +import org.oppia.app.player.state.itemviewmodel.TextInputViewModel +import org.oppia.app.player.state.listener.StateNavigationButtonListener +import org.oppia.app.recyclerview.BindableAdapter import org.oppia.app.viewmodel.ViewModelProvider import org.oppia.domain.audio.CellularDialogController import org.oppia.domain.exploration.ExplorationDataController @@ -38,22 +59,6 @@ import javax.inject.Inject const val STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY = "STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY" private const val TAG_CELLULAR_DATA_DIALOG = "CELLULAR_DATA_DIALOG" private const val TAG_AUDIO_FRAGMENT = "AUDIO_FRAGMENT" -private const val TAG_STATE_FRAGMENT = "STATE_FRAGMENT" - -private const val CONTINUE = "Continue" -private const val END_EXPLORATION = "EndExploration" -@Suppress("unused") -private const val LEARN_AGAIN = "LearnAgain" -private const val MULTIPLE_CHOICE_INPUT = "MultipleChoiceInput" -private const val ITEM_SELECT_INPUT = "ItemSelectionInput" -private const val TEXT_INPUT = "TextInput" -private const val FRACTION_INPUT = "FractionInput" -private const val NUMERIC_INPUT = "NumericInput" -private const val NUMERIC_WITH_UNITS = "NumberWithUnits" - -// For context: -// https://github.com/oppia/oppia/blob/37285a/extensions/interactions/Continue/directives/oppia-interactive-continue.directive.ts -private const val DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER = "Please continue." /** The presenter for [StateFragment]. */ @FragmentScope @@ -62,42 +67,26 @@ class StateFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val cellularDialogController: CellularDialogController, - private val stateButtonViewModelProvider: ViewModelProvider, private val viewModelProvider: ViewModelProvider, private val explorationDataController: ExplorationDataController, private val explorationProgressController: ExplorationProgressController, private val logger: Logger, - private val htmlParserFactory: HtmlParser.Factory -) : ButtonInteractionListener { + private val htmlParserFactory: HtmlParser.Factory, + private val context: Context, + private val interactionViewModelFactoryMap: Map +) : StateNavigationButtonListener { private var showCellularDataDialog = true private var useCellularData = false private lateinit var explorationId: String + private lateinit var currentStateName: String + private lateinit var recyclerViewAdapter: RecyclerView.Adapter<*> + private lateinit var viewModel: StateViewModel + private val ephemeralStateLiveData: LiveData> by lazy { + explorationProgressController.getCurrentState() + } - // TODO(#257): Remove this once domain layer is capable to provide this information. - private val oldStateNameList: ArrayList = ArrayList() - - private lateinit var currentEphemeralState: EphemeralState - private var currentAnswerOutcome: AnswerOutcome? = null - - private val itemList: MutableList = ArrayList() - - // TODO(#257): Remove this once domain layer is capable to provide this information. - private var hasGeneralContinueButton: Boolean = false - - private lateinit var stateAdapter: StateAdapter - - private lateinit var binding: StateFragmentBinding - - private var selectedInputItemIndexes = ArrayList() - - private lateinit var selectInputItemsListener: SelectInputItemsListener - - fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?, selectedInputItemIndexes: ArrayList, selectInputItemsListener: SelectInputItemsListener): View? { - - this.selectedInputItemIndexes = selectedInputItemIndexes - this.selectInputItemsListener = selectInputItemsListener - + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { cellularDialogController.getCellularDataPreference() .observe(fragment, Observer> { if (it.isSuccess()) { @@ -107,14 +96,16 @@ class StateFragmentPresenter @Inject constructor( } }) explorationId = fragment.arguments!!.getString(STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY)!! - stateAdapter = StateAdapter(logger, itemList, this as ButtonInteractionListener, htmlParserFactory, entityType, explorationId, selectedInputItemIndexes, selectInputItemsListener) - binding = StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) + val binding = StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) + val stateRecyclerViewAdapter = createRecyclerViewAdapter() binding.stateRecyclerView.apply { - adapter = stateAdapter + adapter = stateRecyclerViewAdapter } + recyclerViewAdapter = stateRecyclerViewAdapter + viewModel = getStateViewModel() binding.let { - it.stateFragment = fragment as StateFragment - it.viewModel = getStateViewModel() + it.lifecycleOwner = fragment + it.viewModel = this.viewModel } subscribeToCurrentState() @@ -122,6 +113,85 @@ class StateFragmentPresenter @Inject constructor( return binding.root } + private fun createRecyclerViewAdapter(): BindableAdapter { + return BindableAdapter.Builder + .newBuilder() + .registerViewTypeComputer { viewModel -> + when (viewModel) { + is StateNavigationButtonViewModel -> ViewType.VIEW_TYPE_STATE_NAVIGATION_BUTTON.ordinal + is ContentViewModel -> ViewType.VIEW_TYPE_CONTENT.ordinal + is FeedbackViewModel -> ViewType.VIEW_TYPE_FEEDBACK.ordinal + is ContinueInteractionViewModel -> ViewType.VIEW_TYPE_CONTINUE_INTERACTION.ordinal + is SelectionInteractionViewModel -> ViewType.VIEW_TYPE_SELECTION_INTERACTION.ordinal + is FractionInteractionViewModel -> ViewType.VIEW_TYPE_FRACTION_INPUT_INTERACTION.ordinal + is NumericInputViewModel -> ViewType.VIEW_TYPE_NUMERIC_INPUT_INTERACTION.ordinal + is TextInputViewModel -> ViewType.VIEW_TYPE_TEXT_INPUT_INTERACTION.ordinal + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") + } + } + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_STATE_NAVIGATION_BUTTON.ordinal, + inflateDataBinding = StateButtonItemBinding::inflate, + setViewModel = StateButtonItemBinding::setButtonViewModel, + transformViewModel = { it as StateNavigationButtonViewModel } + ) + .registerViewBinder( + viewType = ViewType.VIEW_TYPE_CONTENT.ordinal, + inflateView = { parent -> + ContentItemBinding.inflate(LayoutInflater.from(parent.context), parent, /* attachToParent= */ false).root + }, + bindView = { view, viewModel -> + val binding = DataBindingUtil.findBinding(view)!! + binding.htmlContent = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( + (viewModel as ContentViewModel).htmlContent.toString(), binding.contentTextView + ) + } + ) + .registerViewBinder( + viewType = ViewType.VIEW_TYPE_FEEDBACK.ordinal, + inflateView = { parent -> + FeedbackItemBinding.inflate(LayoutInflater.from(parent.context), parent, /* attachToParent= */ false).root + }, + bindView = { view, viewModel -> + val binding = DataBindingUtil.findBinding(view)!! + binding.htmlContent = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml( + (viewModel as FeedbackViewModel).htmlContent.toString(), binding.feedbackTextView + ) + } + ) + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_CONTINUE_INTERACTION.ordinal, + inflateDataBinding = ContinueInteractionItemBinding::inflate, + setViewModel = ContinueInteractionItemBinding::setViewModel, + transformViewModel = { it as ContinueInteractionViewModel } + ) + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_SELECTION_INTERACTION.ordinal, + inflateDataBinding = SelectionInteractionItemBinding::inflate, + setViewModel = SelectionInteractionItemBinding::setViewModel, + transformViewModel = { it as SelectionInteractionViewModel } + ) + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_FRACTION_INPUT_INTERACTION.ordinal, + inflateDataBinding = FractionInteractionItemBinding::inflate, + setViewModel = FractionInteractionItemBinding::setViewModel, + transformViewModel = { it as FractionInteractionViewModel } + ) + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_NUMERIC_INPUT_INTERACTION.ordinal, + inflateDataBinding = NumericInputInteractionItemBinding::inflate, + setViewModel = NumericInputInteractionItemBinding::setViewModel, + transformViewModel = { it as NumericInputViewModel } + ) + .registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_TEXT_INPUT_INTERACTION.ordinal, + inflateDataBinding = TextInputInteractionItemBinding::inflate, + setViewModel = TextInputInteractionItemBinding::setViewModel, + transformViewModel = { it as TextInputViewModel } + ) + .build() + } + fun handleAudioClick() { if (showCellularDataDialog) { showHideAudioFragment(false) @@ -148,6 +218,11 @@ class StateFragmentPresenter @Inject constructor( } } + fun handleAnswerReadyForSubmission(answer: InteractionObject) { + // An interaction has indicated that an answer is ready for submission. + handleSubmitAnswer(answer) + } + private fun showCellularDataDialogFragment() { val previousFragment = fragment.childFragmentManager.findFragmentByTag(TAG_CELLULAR_DATA_DIALOG) if (previousFragment != null) { @@ -168,7 +243,7 @@ class StateFragmentPresenter @Inject constructor( private fun showHideAudioFragment(isVisible: Boolean) { if (isVisible) { if (getAudioFragment() == null) { - val audioFragment = AudioFragment.newInstance(explorationId, "END") + val audioFragment = AudioFragment.newInstance(explorationId, currentStateName) fragment.childFragmentManager.beginTransaction().add( R.id.audio_fragment_placeholder, audioFragment, TAG_AUDIO_FRAGMENT @@ -182,54 +257,55 @@ class StateFragmentPresenter @Inject constructor( } private fun subscribeToCurrentState() { - ephemeralStateLiveData.observe(fragment, Observer { result -> - itemList.clear() - currentEphemeralState = result - checkAndAddContentItem() - addInteractionForPendingState() - updateDummyStateName() - - val interactionId = result.state.interaction.id - val hasPreviousState = result.hasPreviousState - var canContinueToNextState = false - hasGeneralContinueButton = false - - if (result.stateTypeCase != EphemeralState.StateTypeCase.TERMINAL_STATE) { - if (result.stateTypeCase == EphemeralState.StateTypeCase.COMPLETED_STATE - && !oldStateNameList.contains(result.state.name) - ) { - hasGeneralContinueButton = true - canContinueToNextState = false - } else if (result.completedState.answerList.size > 0 - && oldStateNameList.contains(result.state.name) - ) { - canContinueToNextState = true - hasGeneralContinueButton = false - } - } - - updateNavigationButtonVisibility( - interactionId, - hasPreviousState, - canContinueToNextState, - hasGeneralContinueButton - ) + ephemeralStateLiveData.observe(fragment, Observer> { result -> + processEphemeralStateResult(result) }) } - private val ephemeralStateLiveData: LiveData by lazy { - getEphemeralState() - } + private fun processEphemeralStateResult(result: AsyncResult) { + if (result.isFailure()) { + logger.e("StateFragment", "Failed to retrieve ephemeral state", result.getErrorOrNull()!!) + return + } else if (result.isPending()) { + // Display nothing until a valid result is available. + return + } - private fun getEphemeralState(): LiveData { - return Transformations.map(explorationProgressController.getCurrentState(), ::processCurrentState) - } + val ephemeralState = result.getOrThrow() + currentStateName = ephemeralState.state.name + val pendingItemList = mutableListOf() + addContentItem(pendingItemList, ephemeralState) + val interaction = ephemeralState.state.interaction + if (ephemeralState.stateTypeCase == EphemeralState.StateTypeCase.PENDING_STATE) { + addPreviousAnswers(pendingItemList, interaction, ephemeralState.pendingState.wrongAnswerList) + addInteractionForPendingState(pendingItemList, interaction) + } else if (ephemeralState.stateTypeCase == EphemeralState.StateTypeCase.COMPLETED_STATE) { + addPreviousAnswers(pendingItemList, interaction, ephemeralState.completedState.answerList) + } - private fun processCurrentState(ephemeralStateResult: AsyncResult): EphemeralState { - if (ephemeralStateResult.isFailure()) { - logger.e("StateFragment", "Failed to retrieve ephemeral state", ephemeralStateResult.getErrorOrNull()!!) + val hasPreviousState = ephemeralState.hasPreviousState + var canContinueToNextState = false + var hasGeneralContinueButton = false + + if (ephemeralState.stateTypeCase != EphemeralState.StateTypeCase.TERMINAL_STATE) { + if (ephemeralState.stateTypeCase == EphemeralState.StateTypeCase.COMPLETED_STATE + && !ephemeralState.hasNextState) { + hasGeneralContinueButton = true + } else if (ephemeralState.completedState.answerList.size > 0 && ephemeralState.hasNextState) { + canContinueToNextState = true + } } - return ephemeralStateResult.getOrDefault(EphemeralState.getDefaultInstance()) + + updateNavigationButtonVisibility( + pendingItemList, + hasPreviousState, + canContinueToNextState, + hasGeneralContinueButton, + ephemeralState.stateTypeCase == EphemeralState.StateTypeCase.TERMINAL_STATE + ) + + viewModel.itemList.clear() + viewModel.itemList += pendingItemList } /** @@ -239,17 +315,9 @@ class StateFragmentPresenter @Inject constructor( */ private fun subscribeToAnswerOutcome(answerOutcomeResultLiveData: LiveData>) { val answerOutcomeLiveData = getAnswerOutcome(answerOutcomeResultLiveData) - answerOutcomeLiveData.observe(fragment, Observer { - currentAnswerOutcome = it - - // 'CONTINUE' button has two different types of functionality in different scenarios. - // If the interaction-id is 'Continue', then learner can click the 'CONTINUE' button which will submit an answer - // and move to next state. In other cases, learner submits an answer and if the answer is correct than the `SUBMIT` - // button changes to 'CONTINUE' and in that case click on 'CONTINUE' button does not submit any answer and - // directly moves to next state. - // Here, after submitting an answer it checks whether the interaction-id was 'Continue', if it is continue then move - // to next state. - if (currentEphemeralState.state.interaction.id == CONTINUE) { + answerOutcomeLiveData.observe(fragment, Observer { result -> + // If the answer was submitted on behalf of the Continue interaction, automatically continue to the next state. + if (result.state.interaction.id == "Continue") { moveToNextState() } }) @@ -268,165 +336,116 @@ class StateFragmentPresenter @Inject constructor( return ephemeralStateResult.getOrDefault(AnswerOutcome.getDefaultInstance()) } - private fun endExploration() { + override fun onReturnToTopicButtonClicked() { + hideKeyboard() explorationDataController.stopPlayingExploration() - (activity as ExplorationActivity).finish() + activity.finish() } - override fun onInteractionButtonClicked() { + override fun onSubmitButtonClicked() { hideKeyboard() - // TODO(#163): Remove these dummy answers and fetch answers from different interaction views. - // NB: This sample data will work only with TEST_EXPLORATION_ID_5 - // 0 -> What Language - // 2 -> Welcome! - // XX -> What Language - val stateWelcomeAnswer = 0 - // finnish -> Numeric input - // suomi -> Numeric input - // XX -> What Language - val stateWhatLanguageAnswer: String = "finnish" - // 121 -> Things You can do - // < 121 -> Estimate 100 - // > 121 -> Numeric Input - // XX -> Numeric Input - val stateNumericInputAnswer = 121 - - if (!hasGeneralContinueButton) { - val interactionObject: InteractionObject = getDummyInteractionObject() - when (currentEphemeralState.state.interaction.id) { - END_EXPLORATION -> endExploration() - CONTINUE -> subscribeToAnswerOutcome(explorationProgressController.submitAnswer(createContinueButtonAnswer())) - MULTIPLE_CHOICE_INPUT -> subscribeToAnswerOutcome( - explorationProgressController.submitAnswer( - InteractionObject.newBuilder().setNonNegativeInt( - stateWelcomeAnswer - ).build() - ) - ) - FRACTION_INPUT, - ITEM_SELECT_INPUT, - NUMERIC_INPUT, - NUMERIC_WITH_UNITS, - TEXT_INPUT -> subscribeToAnswerOutcome( - explorationProgressController.submitAnswer(interactionObject) - ) - } - } else { - moveToNextState() - } + handleSubmitAnswer(viewModel.getPendingAnswer()) + } + + override fun onContinueButtonClicked() { + hideKeyboard() + moveToNextState() + } + + private fun handleSubmitAnswer(answer: InteractionObject) { + subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer)) } override fun onPreviousButtonClicked() { explorationProgressController.moveToPreviousState() } - override fun onNextButtonClicked() { - moveToNextState() - } + override fun onNextButtonClicked() = moveToNextState() private fun moveToNextState() { - checkAndUpdateOldStateNameList() - itemList.clear() - currentAnswerOutcome = null explorationProgressController.moveToNextState() } - private fun createContinueButtonAnswer(): InteractionObject { - return InteractionObject.newBuilder().setNormalizedString(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER).build() + private fun addInteractionForPendingState( + pendingItemList: MutableList, interaction: Interaction + ) = addInteraction(pendingItemList, interaction) + + private fun addInteractionForCompletedState( + pendingItemList: MutableList, interaction: Interaction, existingAnswer: InteractionObject + ) = addInteraction(pendingItemList, interaction, existingAnswer = existingAnswer, isReadOnly = true) + + private fun addInteraction( + pendingItemList: MutableList, interaction: Interaction, existingAnswer: + InteractionObject? = null, isReadOnly: Boolean = false) { + val interactionViewModelFactory = interactionViewModelFactoryMap.getValue(interaction.id) + pendingItemList += interactionViewModelFactory( + explorationId, interaction, fragment as InteractionAnswerReceiver, existingAnswer, isReadOnly + ) } - private fun checkAndUpdateOldStateNameList() { - if (currentAnswerOutcome != null - && !currentAnswerOutcome!!.sameState - && !oldStateNameList.contains(currentEphemeralState.state.name) - ) { - oldStateNameList.add(currentEphemeralState.state.name) - } - } - - private fun checkAndAddContentItem() { - if (currentEphemeralState.state.hasContent()) { - addContentItem() - } else { - logger.e("StateFragment", "checkAndAddContentItem: State does not have content.") - } + private fun addContentItem(pendingItemList: MutableList, ephemeralState: EphemeralState) { + val contentSubtitledHtml: SubtitledHtml = ephemeralState.state.content + pendingItemList += ContentViewModel(contentSubtitledHtml.html) } - private fun addContentItem() { - val contentViewModel = ContentViewModel() - val contentSubtitledHtml: SubtitledHtml = currentEphemeralState.state.content - contentViewModel.contentId = contentSubtitledHtml.contentId - contentViewModel.htmlContent = contentSubtitledHtml.html - itemList.add(contentViewModel) - stateAdapter.notifyDataSetChanged() - } - - private fun addInteractionForPendingState() { - if (currentEphemeralState.stateTypeCase.number == EphemeralState.PENDING_STATE_FIELD_NUMBER) { - when (currentEphemeralState.state.interaction.id) { - MULTIPLE_CHOICE_INPUT, ITEM_SELECT_INPUT -> { - addSelectionInteraction() - } - } + private fun addPreviousAnswers( + pendingItemList: MutableList, interaction: Interaction, + answersAndResponses: List + ) { + // TODO: add support for displaying the previous answer, too. + for (answerAndResponse in answersAndResponses) { + addInteractionForCompletedState(pendingItemList, interaction, answerAndResponse.userAnswer) + addFeedbackItem(pendingItemList, answerAndResponse.feedback) } } - private fun addSelectionInteraction() { - val customizationArgsMap: Map = - currentEphemeralState.state.interaction.customizationArgsMap - val multipleChoiceInputInteractionViewModel = SelectionInteractionCustomizationArgsViewModel() - val allKeys: Set = customizationArgsMap.keys - - for (key in allKeys) { - logger.d(TAG_STATE_FRAGMENT, key) - } - if (customizationArgsMap.contains("choices")) { - if (customizationArgsMap.contains("maxAllowableSelectionCount")) { - multipleChoiceInputInteractionViewModel.maxAllowableSelectionCount = - currentEphemeralState.state.interaction.customizationArgsMap["maxAllowableSelectionCount"]!!.signedInt - multipleChoiceInputInteractionViewModel.minAllowableSelectionCount = - currentEphemeralState.state.interaction.customizationArgsMap["minAllowableSelectionCount"]!!.signedInt - } - multipleChoiceInputInteractionViewModel.interactionId = currentEphemeralState.state.interaction.id - multipleChoiceInputInteractionViewModel.choiceItems = - currentEphemeralState.state.interaction.customizationArgsMap["choices"]!!.setOfHtmlString.htmlList + private fun addFeedbackItem(pendingItemList: MutableList, feedback: SubtitledHtml) { + // Only show feedback if there's some to show. + if (feedback.html.isNotEmpty()) { + pendingItemList += FeedbackViewModel(feedback.html) } - itemList.add(multipleChoiceInputInteractionViewModel) - stateAdapter.notifyDataSetChanged() } private fun updateNavigationButtonVisibility( - interactionId: String, + pendingItemList: MutableList, hasPreviousState: Boolean, canContinueToNextState: Boolean, - hasGeneralContinueButton: Boolean + hasGeneralContinueButton: Boolean, + stateIsTerminal: Boolean ) { - getStateButtonViewModel().setPreviousButtonVisible(hasPreviousState) + val stateNavigationButtonViewModel = StateNavigationButtonViewModel(context, this as StateNavigationButtonListener) + stateNavigationButtonViewModel.updatePreviousButton(isEnabled = hasPreviousState) + // Set continuation button. when { hasGeneralContinueButton -> { - getStateButtonViewModel().clearObservableInteractionId() - getStateButtonViewModel().setObservableInteractionId(CONTINUE) + stateNavigationButtonViewModel.updateContinuationButton( + ContinuationNavigationButtonType.CONTINUE_BUTTON, isEnabled = true + ) } canContinueToNextState -> { - getStateButtonViewModel().clearObservableInteractionId() - getStateButtonViewModel().setNextButtonVisible(canContinueToNextState) + stateNavigationButtonViewModel.updateContinuationButton( + ContinuationNavigationButtonType.NEXT_BUTTON, isEnabled = canContinueToNextState + ) + } + stateIsTerminal -> { + stateNavigationButtonViewModel.updateContinuationButton( + ContinuationNavigationButtonType.RETURN_TO_TOPIC_BUTTON, isEnabled = true + ) + } + viewModel.doesMostRecentInteractionRequireExplicitSubmission(pendingItemList) -> { + stateNavigationButtonViewModel.updateContinuationButton( + ContinuationNavigationButtonType.SUBMIT_BUTTON, isEnabled = true + ) } else -> { - getStateButtonViewModel().setObservableInteractionId(interactionId) - // TODO(#163): This function controls whether the "Submit" button should be displayed or not. - // Remove this function in final implementation and control this whenever user selects some option in - // MultipleChoiceInput or InputSelectionInput. For now this is `true` because we do not have a mechanism to work - // with MultipleChoiceInput or InputSelectionInput, which will eventually be responsible for controlling this. - getStateButtonViewModel().optionSelected(true) + // No continuation button needs to be set since the interaction itself will push for answer submission. + stateNavigationButtonViewModel.updateContinuationButton( + ContinuationNavigationButtonType.CONTINUE_BUTTON, isEnabled = false + ) } } - itemList.add(getStateButtonViewModel()) - stateAdapter.notifyDataSetChanged() - } - - private fun getStateButtonViewModel(): StateButtonViewModel { - return stateButtonViewModelProvider.getForFragment(fragment, StateButtonViewModel::class.java) + pendingItemList += stateNavigationButtonViewModel } private fun hideKeyboard() { @@ -434,21 +453,14 @@ class StateFragmentPresenter @Inject constructor( inputManager.hideSoftInputFromWindow(fragment.view!!.windowToken, InputMethodManager.SHOW_FORCED) } - // TODO(#163): Remove this function, this is just for dummy testing purposes. - private fun updateDummyStateName() { - getStateViewModel().setStateName(currentEphemeralState.state.name) - } - - // TODO(#163): Remove this function and fetch this InteractionObject from [StateAdapter]. - private fun getDummyInteractionObject(): InteractionObject { - val interactionObjectBuilder: InteractionObject.Builder = InteractionObject.newBuilder() - when (currentEphemeralState.state.name) { - "Welcome!" -> interactionObjectBuilder.nonNegativeInt = 0 - "What language" -> interactionObjectBuilder.normalizedString = "finnish" - "Things you can do" -> createContinueButtonAnswer() - "Numeric input" -> interactionObjectBuilder.real = 121.0 - else -> InteractionObject.getDefaultInstance() - } - return interactionObjectBuilder.build() + private enum class ViewType { + VIEW_TYPE_CONTENT, + VIEW_TYPE_FEEDBACK, + VIEW_TYPE_STATE_NAVIGATION_BUTTON, + VIEW_TYPE_CONTINUE_INTERACTION, + VIEW_TYPE_SELECTION_INTERACTION, + VIEW_TYPE_FRACTION_INPUT_INTERACTION, + VIEW_TYPE_NUMERIC_INPUT_INTERACTION, + VIEW_TYPE_TEXT_INPUT_INTERACTION } } diff --git a/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt b/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt index 042e2ddefb5..bef2898310e 100644 --- a/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt @@ -1,17 +1,37 @@ package org.oppia.app.player.state -import androidx.databinding.ObservableField +import androidx.databinding.ObservableArrayList +import androidx.databinding.ObservableList import androidx.lifecycle.ViewModel import org.oppia.app.fragment.FragmentScope +import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.app.viewmodel.ObservableViewModel import javax.inject.Inject /** [ViewModel] for state-fragment. */ @FragmentScope class StateViewModel @Inject constructor() : ObservableViewModel() { - var stateName = ObservableField() + val itemList: ObservableList = ObservableArrayList() - fun setStateName(state: String) { - stateName.set(state) + /** + * Returns whether there is currently a pending interaction that requires an additional user action to submit the + * answer. + */ + fun doesMostRecentInteractionRequireExplicitSubmission(itemList: List): Boolean { + return getPendingAnswerHandler(itemList)?.isExplicitAnswerSubmissionRequired() ?: true + } + + // TODO(#164): Add a hasPendingAnswer() that binds to the enabled state of the Submit button. + fun getPendingAnswer(): InteractionObject { + return getPendingAnswerHandler(itemList)?.getPendingAnswer() ?: InteractionObject.getDefaultInstance() + } + + private fun getPendingAnswerHandler(itemList: List): InteractionAnswerHandler? { + // Search through all items to find the latest InteractionAnswerHandler which should be the pending one. In the + // future, it may be ideal to make this more robust by actually tracking the handler corresponding to the pending + // interaction. + return itemList.findLast { it is InteractionAnswerHandler } as? InteractionAnswerHandler } } diff --git a/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt b/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt new file mode 100644 index 00000000000..cc196bcfe42 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt @@ -0,0 +1,27 @@ +package org.oppia.app.player.state.answerhandling + +import org.oppia.app.model.InteractionObject + +/** + * A handler for interaction answers. Handlers can either require an additional user action before the answer can be + * processed, or they can push the answer directly to a [InteractionAnswerReceiver]. Implementations must indicate + * whether they require an explicit submit button. + */ +interface InteractionAnswerHandler { + /** + * Returns whether this handler requires explicit answer submission. Note that this is expected to be an invariant for + * the lifetime of this handler instance. + */ + fun isExplicitAnswerSubmissionRequired(): Boolean = true + + /** Return the current answer that is ready for handling. */ + fun getPendingAnswer(): InteractionObject +} + +/** + * A callback that will be called by [InteractionAnswerHandler]s when a user submits an answer. To be implemented by + * the parent fragment of the handler. + */ +interface InteractionAnswerReceiver { + fun onAnswerReadyForSubmission(answer: InteractionObject) +} diff --git a/app/src/main/java/org/oppia/app/player/state/customview/SelectionInputInteractionView.kt b/app/src/main/java/org/oppia/app/player/state/customview/SelectionInputInteractionView.kt deleted file mode 100755 index c5613eb5cc9..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/customview/SelectionInputInteractionView.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.oppia.app.player.state.customview - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.recyclerview.widget.RecyclerView -import org.oppia.app.R -import org.oppia.app.model.InteractionObject -import org.oppia.app.player.state.InteractionAdapter -import org.oppia.app.player.state.listener.InteractionAnswerRetriever -import org.oppia.app.player.state.listener.ItemClickListener - -internal class SelectionInputInteractionView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defaultStyle: Int = 0 -) : FrameLayout(context, attrs, defaultStyle), ItemClickListener, InteractionAnswerRetriever { - private var interactionObjectBuilder: InteractionObject = InteractionObject.newBuilder().build() - - override fun onItemClick(interactionObject: InteractionObject) { - interactionObjectBuilder = interactionObject - } - - private val recyclerView: RecyclerView = RecyclerView(context, attrs, defaultStyle) - - init { - recyclerView.id = R.id.selection_interaction_recyclerview - val params = LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT - ) - this.addView(recyclerView, params) - isClickable = true - isFocusable = true - } - - internal fun setAdapter(adapter: InteractionAdapter) { - recyclerView.adapter = adapter - } - - override fun getPendingAnswer(): InteractionObject { - return interactionObjectBuilder - } -} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt old mode 100755 new mode 100644 index 8606d70ae38..5ba6823546c --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt @@ -1,12 +1,4 @@ package org.oppia.app.player.state.itemviewmodel -import androidx.lifecycle.ViewModel -import org.oppia.app.fragment.FragmentScope -import javax.inject.Inject - /** [ViewModel] for content-card state. */ -@FragmentScope -class ContentViewModel @Inject constructor() : ViewModel() { - var contentId = "" - var htmlContent = "" -} +class ContentViewModel(val htmlContent: CharSequence): StateItemViewModel() diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt new file mode 100644 index 00000000000..07b49d040a6 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt @@ -0,0 +1,28 @@ +package org.oppia.app.player.state.itemviewmodel + +import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver +import org.oppia.domain.util.toAnswerString + +// For context: +// https://github.com/oppia/oppia/blob/37285a/extensions/interactions/Continue/directives/oppia-interactive-continue.directive.ts +private const val DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER = "Please continue." + +/** [ViewModel] for the 'Continue' button. */ +class ContinueInteractionViewModel( + private val interactionAnswerReceiver: InteractionAnswerReceiver, existingAnswer: InteractionObject?, + val isReadOnly: Boolean +): StateItemViewModel(), InteractionAnswerHandler { + val answerText: CharSequence = existingAnswer?.toAnswerString() ?: "" + + override fun isExplicitAnswerSubmissionRequired(): Boolean = false + + override fun getPendingAnswer(): InteractionObject { + return InteractionObject.newBuilder().setNormalizedString(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER).build() + } + + fun handleButtonClicked() { + interactionAnswerReceiver.onAnswerReadyForSubmission(getPendingAnswer()) + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FeedbackViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FeedbackViewModel.kt new file mode 100644 index 00000000000..2cd98205645 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FeedbackViewModel.kt @@ -0,0 +1,4 @@ +package org.oppia.app.player.state.itemviewmodel + +/** [ViewModel] for feedback blurbs. */ +class FeedbackViewModel(val htmlContent: CharSequence): StateItemViewModel() diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt new file mode 100644 index 00000000000..96a5b61243f --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt @@ -0,0 +1,20 @@ +package org.oppia.app.player.state.itemviewmodel + +import org.oppia.app.model.InteractionObject +import org.oppia.app.parser.StringToFractionParser +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.domain.util.toAnswerString + +class FractionInteractionViewModel( + existingAnswer: InteractionObject?, val isReadOnly: Boolean +): StateItemViewModel(), InteractionAnswerHandler { + var answerText: CharSequence = existingAnswer?.toAnswerString() ?: "" + + override fun getPendingAnswer(): InteractionObject { + val interactionObjectBuilder = InteractionObject.newBuilder() + if (answerText.isNotEmpty()) { + interactionObjectBuilder.fraction = StringToFractionParser().getFractionFromString(answerText.toString()) + } + return interactionObjectBuilder.build() + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt new file mode 100644 index 00000000000..dbaa0fe87b5 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt @@ -0,0 +1,15 @@ +package org.oppia.app.player.state.itemviewmodel + +import org.oppia.app.model.Interaction +import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver + +/** + * Returns a new [StateItemViewModel] corresponding to this interaction with an initial, optional answer filled in, + * optionally read-only (e.g. if the interaction is no longer accepting new answers), a receiver for answers if this + * interaction pushes answers, the [Interaction] object corresponding to the interaction view, and the exploration ID. + */ +typealias InteractionViewModelFactory = ( + explorationId: String, interaction: Interaction, interactionAnswerReceiver: InteractionAnswerReceiver, + existingAnswer: InteractionObject?, isReadOnly: Boolean +) -> StateItemViewModel diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt new file mode 100644 index 00000000000..b2c1038d4ee --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt @@ -0,0 +1,59 @@ +package org.oppia.app.player.state.itemviewmodel + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey + +/** + * Module to provide interaction view model-specific dependencies for intreactions that should be explicitly displayed + * to the user. + */ +@Module +class 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 + @IntoMap + @StringKey("Continue") + fun provideContinueInteractionViewModelFactory(): InteractionViewModelFactory { + return { _, _, interactionAnswerReceiver, existingAnswer, isReadOnly -> + ContinueInteractionViewModel(interactionAnswerReceiver, existingAnswer, isReadOnly) + } + } + + @Provides + @IntoMap + @StringKey("MultipleChoiceInput") + fun provideMultipleChoiceInputViewModelFactory(): InteractionViewModelFactory { + return ::SelectionInteractionViewModel + } + + @Provides + @IntoMap + @StringKey("ItemSelectionInput") + fun provideItemSelectionInputViewModelFactory(): InteractionViewModelFactory { + return ::SelectionInteractionViewModel + } + + @Provides + @IntoMap + @StringKey("FractionInput") + fun provideFractionInputViewModelFactory(): InteractionViewModelFactory { + return { _, _, _, existingAnswer, isReadOnly -> FractionInteractionViewModel(existingAnswer, isReadOnly) } + } + + @Provides + @IntoMap + @StringKey("NumericInput") + fun provideNumericInputViewModelFactory(): InteractionViewModelFactory { + return { _, _, _, existingAnswer, isReadOnly -> NumericInputViewModel(existingAnswer, isReadOnly) } + } + + @Provides + @IntoMap + @StringKey("TextInput") + fun provideTextInputViewModelFactory(): InteractionViewModelFactory { + return { _, _, _, existingAnswer, isReadOnly -> TextInputViewModel(existingAnswer, isReadOnly) } + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/NumericInputViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/NumericInputViewModel.kt new file mode 100644 index 00000000000..f37881b3cc8 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/NumericInputViewModel.kt @@ -0,0 +1,19 @@ +package org.oppia.app.player.state.itemviewmodel + +import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.domain.util.toAnswerString + +class NumericInputViewModel( + existingAnswer: InteractionObject?, val isReadOnly: Boolean +): StateItemViewModel(), InteractionAnswerHandler { + var answerText: CharSequence = existingAnswer?.toAnswerString() ?: "" + + override fun getPendingAnswer(): InteractionObject { + val interactionObjectBuilder = InteractionObject.newBuilder() + if (answerText.isNotEmpty()) { + interactionObjectBuilder.real = answerText.toString().toDouble() + } + return interactionObjectBuilder.build() + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt old mode 100755 new mode 100644 index 5bfb9e5b059..b65112e3acd --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt @@ -1,9 +1,20 @@ package org.oppia.app.player.state.itemviewmodel -import androidx.lifecycle.ViewModel +import androidx.databinding.ObservableBoolean +import org.oppia.app.viewmodel.ObservableViewModel -/** [ViewModel] for MultipleChoiceInput values or ItemSelectionInput values. */ -class SelectionInteractionContentViewModel : ViewModel() { - var htmlContent: String = "" - var isAnswerSelected = false +/** [ObservableViewModel] for MultipleChoiceInput values or ItemSelectionInput values. */ +class SelectionInteractionContentViewModel( + val htmlContent: String, private val itemIndex: Int, isAnswerInitiallySelected: Boolean, val isReadOnly: Boolean, + private val selectionInteractionViewModel: SelectionInteractionViewModel +): ObservableViewModel() { + var isAnswerSelected = ObservableBoolean(isAnswerInitiallySelected) + + fun handleItemClicked() { + val isCurrentlySelected = isAnswerSelected.get() + val shouldNowBeSelected = selectionInteractionViewModel.updateSelection(itemIndex, isCurrentlySelected) + if (isCurrentlySelected != shouldNowBeSelected) { + isAnswerSelected.set(shouldNowBeSelected) + } + } } diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionCustomizationArgsViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionCustomizationArgsViewModel.kt deleted file mode 100755 index fbc4f4c1d38..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionCustomizationArgsViewModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.oppia.app.player.state.itemviewmodel - -import androidx.lifecycle.ViewModel - -/** [ViewModel] for multiple or item-selection input choice list. */ -class SelectionInteractionCustomizationArgsViewModel : ViewModel() { - var choiceItems: MutableList? = null - var interactionId: String = "" - var maxAllowableSelectionCount: Int = 0 - var minAllowableSelectionCount: Int = 0 -} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt new file mode 100644 index 00000000000..e52c4e31f37 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -0,0 +1,123 @@ +package org.oppia.app.player.state.itemviewmodel + +import androidx.databinding.ObservableArrayList +import androidx.databinding.ObservableList +import org.oppia.app.model.Interaction +import org.oppia.app.model.InteractionObject +import org.oppia.app.model.StringList +import org.oppia.app.player.state.SelectionItemInputType +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver + +/** ViewModel for multiple or item-selection input choice list. */ +class SelectionInteractionViewModel( + val explorationId: String, interaction: Interaction, private val interactionAnswerReceiver: InteractionAnswerReceiver, + existingAnswer: InteractionObject?, val isReadOnly: Boolean +): StateItemViewModel(), InteractionAnswerHandler { + private val interactionId: String = interaction.id + private val choiceStrings: List by lazy { + interaction.customizationArgsMap["choices"]?.setOfHtmlString?.htmlList ?: listOf() + } + private val minAllowableSelectionCount: Int by lazy { + interaction.customizationArgsMap["minAllowableSelectionCount"]?.signedInt ?: 1 + } + private val maxAllowableSelectionCount: Int by lazy { + // Assume that at least 1 answer always needs to be submitted, and that the max can't be less than the min for cases + // when either of the counts are not specified. + interaction.customizationArgsMap["maxAllowableSelectionCount"]?.signedInt ?: minAllowableSelectionCount + } + private val selectedItems = computeSelectedItems( + existingAnswer ?: InteractionObject.getDefaultInstance(), interactionId, choiceStrings + ) + val choiceItems: ObservableList = computeChoiceItems( + choiceStrings, selectedItems, isReadOnly, this + ) + + override fun isExplicitAnswerSubmissionRequired(): Boolean { + // If more than one answer is allowed, then a submission button is needed. + return maxAllowableSelectionCount > 1 + } + + override fun getPendingAnswer(): InteractionObject { + val interactionObjectBuilder = InteractionObject.newBuilder() + if (interactionId == "ItemSelectionInput") { + interactionObjectBuilder.setOfHtmlString = StringList.newBuilder() + .addAllHtml(selectedItems.map(choiceItems::get).map { it.htmlContent }) + .build() + } else if (selectedItems.size == 1) { + interactionObjectBuilder.nonNegativeInt = selectedItems.first() + } + return interactionObjectBuilder.build() + } + + /** Returns the [SelectionItemInputType] that should be used to render items of this view model. */ + fun getSelectionItemInputType(): SelectionItemInputType { + return if (areCheckboxesBound()) { + SelectionItemInputType.CHECKBOXES + } else { + SelectionItemInputType.RADIO_BUTTONS + } + } + + /** Catalogs an item being clicked by the user and returns whether the item should be considered selected. */ + fun updateSelection(itemIndex: Int, isCurrentlySelected: Boolean): Boolean { + if (areCheckboxesBound()) { + if (isCurrentlySelected) { + selectedItems -= itemIndex + return false + } else if (selectedItems.size < maxAllowableSelectionCount) { + // TODO(#32): Add warning to user when they exceed the number of allowable selections or are under the minimum + // number required. + selectedItems += itemIndex + return true + } + } else { + // Disable all items to simulate a radio button group. + choiceItems.forEach { item -> item.isAnswerSelected.set(false) } + selectedItems.clear() + selectedItems += itemIndex + + // Only push the answer if explicit submission isn't required. + if (maxAllowableSelectionCount == 1) { + interactionAnswerReceiver.onAnswerReadyForSubmission(getPendingAnswer()) + } + return true + } + + // Do not change the current status if it isn't valid to do so. + return isCurrentlySelected + } + + private fun areCheckboxesBound(): Boolean { + return interactionId == "ItemSelectionInput" && maxAllowableSelectionCount > 1 + } + + companion object { + private fun computeSelectedItems( + answer: InteractionObject, interactionId: String, choiceStrings: List + ): MutableList { + return if (interactionId == "ItemSelectionInput") { + answer.setOfHtmlString.htmlList.map(choiceStrings::indexOf).toMutableList() + } else if (answer.objectTypeCase == InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT) { + mutableListOf(answer.nonNegativeInt) + } else { + mutableListOf() + } + } + + private fun computeChoiceItems( + choiceStrings: List, selectedItems: List, isReadOnly: Boolean, + selectionInteractionViewModel: SelectionInteractionViewModel + ): ObservableArrayList { + val observableList = ObservableArrayList() + observableList += choiceStrings.mapIndexed { index, choiceString -> + val isAnswerSelected = index in selectedItems + SelectionInteractionContentViewModel( + htmlContent = choiceString, itemIndex = index, isAnswerInitiallySelected = isAnswerSelected, + isReadOnly = isReadOnly, selectionInteractionViewModel = selectionInteractionViewModel + ) + } + return observableList + } + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateButtonViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateButtonViewModel.kt deleted file mode 100644 index eccdeea22de..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateButtonViewModel.kt +++ /dev/null @@ -1,108 +0,0 @@ -package org.oppia.app.player.state.itemviewmodel - -import android.content.Context -import android.widget.Button -import androidx.databinding.BindingAdapter -import androidx.databinding.ObservableField -import androidx.lifecycle.ViewModel -import org.oppia.app.fragment.FragmentScope -import javax.inject.Inject -import org.oppia.app.R -import org.oppia.app.viewmodel.ObservableViewModel - -private const val CONTINUE = "Continue" -private const val END_EXPLORATION = "EndExploration" -private const val LEARN_AGAIN = "LearnAgain" -private const val MULTIPLE_CHOICE_INPUT = "MultipleChoiceInput" -private const val ITEM_SELECT_INPUT = "ItemSelectionInput" -private const val TEXT_INPUT = "TextInput" -private const val FRACTION_INPUT = "FractionInput" -private const val NUMERIC_INPUT = "NumericInput" -private const val NUMERIC_WITH_UNITS = "NumberWithUnits" - -/** [ViewModel] for state-fragment. */ -@FragmentScope -class StateButtonViewModel @Inject constructor(val context: Context) : ObservableViewModel() { - companion object { - @JvmStatic - @BindingAdapter("android:button") - fun setBackgroundResource(button: Button, resource: Int) { - button.setBackgroundResource(resource) - } - } - - var isAudioFragmentVisible = ObservableField(false) - - var isNextButtonVisible = ObservableField(false) - var isPreviousButtonVisible = ObservableField(false) - - var observableInteractionId = ObservableField() - var isInteractionButtonActive = ObservableField(false) - var isInteractionButtonVisible = ObservableField(false) - var drawableResourceValue = ObservableField(R.drawable.state_button_primary_background) - - var name = ObservableField() - - fun setObservableInteractionId(interactionId: String) { - setNextButtonVisible(false) - observableInteractionId.set(interactionId) - // TODO(#249): Generalize this binding to make adding future interactions easier. - when (interactionId) { - CONTINUE -> { - isInteractionButtonActive.set(true) - isInteractionButtonVisible.set(true) - name.set(context.getString(R.string.state_continue_button)) - drawableResourceValue.set(R.drawable.state_button_primary_background) - } - END_EXPLORATION -> { - isInteractionButtonActive.set(true) - isInteractionButtonVisible.set(true) - name.set(context.getString(R.string.state_end_exploration_button)) - drawableResourceValue.set(R.drawable.state_button_primary_background) - } - LEARN_AGAIN -> { - isInteractionButtonActive.set(true) - isInteractionButtonVisible.set(true) - name.set(context.getString(R.string.state_learn_again_button)) - drawableResourceValue.set(R.drawable.state_button_blue_background) - } - ITEM_SELECT_INPUT, MULTIPLE_CHOICE_INPUT -> { - isInteractionButtonActive.set(true) - isInteractionButtonVisible.set(false) - name.set(context.getString(R.string.state_submit_button)) - drawableResourceValue.set(R.drawable.state_button_primary_background) - } - FRACTION_INPUT, NUMERIC_INPUT, NUMERIC_WITH_UNITS, TEXT_INPUT -> { - // TODO(#163): The value of isInteractionButtonVisible should be false in this case and it should be updated. - // We are keeping this true for now so that the submit button can work even without any interaction. - isInteractionButtonActive.set(true) - isInteractionButtonVisible.set(true) - name.set(context.getString(R.string.state_submit_button)) - // TODO(#163): The value of drawable should be R.drawable.state_button_transparent_background as per above explanation. - drawableResourceValue.set(R.drawable.state_button_primary_background) - } - } - } - - fun clearObservableInteractionId() { - observableInteractionId.set("") - isInteractionButtonVisible.set(false) - isInteractionButtonActive.set(false) - } - - fun setAudioFragmentVisible(isVisible: Boolean) { - isAudioFragmentVisible.set(isVisible) - } - - fun setNextButtonVisible(isVisible: Boolean) { - isNextButtonVisible.set(isVisible) - } - - fun setPreviousButtonVisible(isVisible: Boolean) { - isPreviousButtonVisible.set(isVisible) - } - - fun optionSelected(isOptionSelected: Boolean) { - isInteractionButtonVisible.set(isOptionSelected) - } -} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateItemViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateItemViewModel.kt new file mode 100644 index 00000000000..af30fa463d4 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateItemViewModel.kt @@ -0,0 +1,6 @@ +package org.oppia.app.player.state.itemviewmodel + +import org.oppia.app.viewmodel.ObservableViewModel + +/** The root [ViewModel] for all individual items that may be displayed in the state fragment recycler view. */ +abstract class StateItemViewModel: ObservableViewModel() diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateNavigationButtonViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateNavigationButtonViewModel.kt new file mode 100644 index 00000000000..6de79aac1af --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/StateNavigationButtonViewModel.kt @@ -0,0 +1,100 @@ +package org.oppia.app.player.state.itemviewmodel + +import android.content.Context +import android.widget.Button +import androidx.databinding.BindingAdapter +import androidx.databinding.ObservableField +import androidx.lifecycle.ViewModel +import org.oppia.app.R +import org.oppia.app.player.state.listener.StateNavigationButtonListener + +/** [ViewModel] for State navigation buttons. */ +class StateNavigationButtonViewModel( + val context: Context, val stateNavigationButtonListener: StateNavigationButtonListener +) : StateItemViewModel() { + companion object { + @JvmStatic + @BindingAdapter("android:button") + fun setBackgroundResource(button: Button, resource: Int) { + button.setBackgroundResource(resource) + } + } + + private var currentContinuationNavigationButtonType: ContinuationNavigationButtonType = + ContinuationNavigationButtonType.NO_CONTINUATION_BUTTON + + var isNextButtonVisible = ObservableField(false) + var isPreviousButtonVisible = ObservableField(false) + + var isInteractionButtonActive = ObservableField(false) + var isInteractionButtonVisible = ObservableField(false) + var drawableResourceValue = ObservableField(R.drawable.state_button_primary_background) + + var interactionButtonName = ObservableField() + + fun updatePreviousButton(isEnabled: Boolean) { + isPreviousButtonVisible.set(isEnabled) + } + + fun updateContinuationButton( + continuationNavigationButtonType: ContinuationNavigationButtonType, isEnabled: Boolean + ) { + currentContinuationNavigationButtonType = continuationNavigationButtonType + when (continuationNavigationButtonType) { + ContinuationNavigationButtonType.NEXT_BUTTON -> { + isInteractionButtonActive.set(false) + isInteractionButtonVisible.set(false) + isNextButtonVisible.set(isEnabled) + } + ContinuationNavigationButtonType.SUBMIT_BUTTON -> { + isNextButtonVisible.set(false) + isInteractionButtonActive.set(isEnabled) + isInteractionButtonVisible.set(isEnabled) + interactionButtonName.set(context.getString(R.string.state_submit_button)) + drawableResourceValue.set(R.drawable.state_button_primary_background) + } + ContinuationNavigationButtonType.CONTINUE_BUTTON -> { + isNextButtonVisible.set(false) + isInteractionButtonActive.set(isEnabled) + isInteractionButtonVisible.set(isEnabled) + interactionButtonName.set(context.getString(R.string.state_continue_button)) + drawableResourceValue.set(R.drawable.state_button_primary_background) + } + ContinuationNavigationButtonType.RETURN_TO_TOPIC_BUTTON -> { + isNextButtonVisible.set(false) + isInteractionButtonActive.set(isEnabled) + isInteractionButtonVisible.set(isEnabled) + interactionButtonName.set(context.getString(R.string.state_end_exploration_button)) + drawableResourceValue.set(R.drawable.state_button_primary_background) + } + ContinuationNavigationButtonType.NO_CONTINUATION_BUTTON -> { + isInteractionButtonActive.set(false) + isInteractionButtonVisible.set(false) + isNextButtonVisible.set(false) + } + } + } + + fun triggerContinuationNavigationButtonCallback() { + when (currentContinuationNavigationButtonType) { + ContinuationNavigationButtonType.NEXT_BUTTON -> stateNavigationButtonListener.onNextButtonClicked() + ContinuationNavigationButtonType.SUBMIT_BUTTON -> stateNavigationButtonListener.onSubmitButtonClicked() + ContinuationNavigationButtonType.CONTINUE_BUTTON -> stateNavigationButtonListener.onContinueButtonClicked() + ContinuationNavigationButtonType.RETURN_TO_TOPIC_BUTTON -> { + stateNavigationButtonListener.onReturnToTopicButtonClicked() + } + else -> throw IllegalStateException( + "Cannot trigger continuation for current button state: $currentContinuationNavigationButtonType" + ) + } + } + + /** The type of the state continue navigation button being shown. */ + enum class ContinuationNavigationButtonType { + NO_CONTINUATION_BUTTON, + NEXT_BUTTON, + SUBMIT_BUTTON, + CONTINUE_BUTTON, + RETURN_TO_TOPIC_BUTTON + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/TextInputViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/TextInputViewModel.kt new file mode 100644 index 00000000000..ab473fa85cf --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/TextInputViewModel.kt @@ -0,0 +1,19 @@ +package org.oppia.app.player.state.itemviewmodel + +import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler +import org.oppia.domain.util.toAnswerString + +class TextInputViewModel( + existingAnswer: InteractionObject?, val isReadOnly: Boolean +): StateItemViewModel(), InteractionAnswerHandler { + var answerText: CharSequence = existingAnswer?.toAnswerString() ?: "" + + override fun getPendingAnswer(): InteractionObject { + val interactionObjectBuilder = InteractionObject.newBuilder() + if (answerText.isNotEmpty()) { + interactionObjectBuilder.normalizedString = answerText.toString() + } + return interactionObjectBuilder.build() + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/listener/InputInteractionListener.kt b/app/src/main/java/org/oppia/app/player/state/listener/InputInteractionListener.kt deleted file mode 100644 index 3da5bc02267..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/listener/InputInteractionListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.oppia.app.player.state.listener - -/** - * This interface helps to check if the learner has selected some option, - * or if the learner has started typing in edit-text interactions. - */ -interface InputInteractionTextListener { - fun hasLearnerStartedAnswering(inputInteractionStarted: Boolean) -} diff --git a/app/src/main/java/org/oppia/app/player/state/listener/InteractionAnswerRetriever.kt b/app/src/main/java/org/oppia/app/player/state/listener/InteractionAnswerRetriever.kt deleted file mode 100644 index cb61bbe49c1..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/listener/InteractionAnswerRetriever.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.oppia.app.player.state.listener - -import org.oppia.app.model.InteractionObject - -/** This interface helps to get pending answer of any learner interaction. */ -interface InteractionAnswerRetriever { - fun getPendingAnswer(): InteractionObject -} diff --git a/app/src/main/java/org/oppia/app/player/state/listener/ItemClickListener.kt b/app/src/main/java/org/oppia/app/player/state/listener/ItemClickListener.kt deleted file mode 100755 index 4963a1cd64f..00000000000 --- a/app/src/main/java/org/oppia/app/player/state/listener/ItemClickListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.oppia.app.player.state.listener - -import org.oppia.app.model.InteractionObject - -/** This interface helps to get pending answer of MultipleChoice/ItemSelection input interaction. */ -interface ItemClickListener { - fun onItemClick(interactionObject: InteractionObject) -} diff --git a/app/src/main/java/org/oppia/app/player/state/listener/ButtonInteractionListener.kt b/app/src/main/java/org/oppia/app/player/state/listener/StateNavigationButtonListener.kt similarity index 55% rename from app/src/main/java/org/oppia/app/player/state/listener/ButtonInteractionListener.kt rename to app/src/main/java/org/oppia/app/player/state/listener/StateNavigationButtonListener.kt index e69523f06e9..05c0dfa7f06 100644 --- a/app/src/main/java/org/oppia/app/player/state/listener/ButtonInteractionListener.kt +++ b/app/src/main/java/org/oppia/app/player/state/listener/StateNavigationButtonListener.kt @@ -1,8 +1,10 @@ package org.oppia.app.player.state.listener /** This interface helps to know when a button has been clicked. */ -interface ButtonInteractionListener { +interface StateNavigationButtonListener { fun onPreviousButtonClicked() fun onNextButtonClicked() - fun onInteractionButtonClicked() + fun onReturnToTopicButtonClicked() + fun onSubmitButtonClicked() + fun onContinueButtonClicked() } diff --git a/app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt b/app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt index 8551a091f77..118ced0d6d1 100644 --- a/app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt +++ b/app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt @@ -12,7 +12,7 @@ import javax.inject.Inject class AddProfileFragment : InjectableFragment() { @Inject lateinit var addProfileFragmentPresenter: AddProfileFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/profile/AdminAuthFragment.kt b/app/src/main/java/org/oppia/app/profile/AdminAuthFragment.kt index bdfc867d565..8fabb431deb 100644 --- a/app/src/main/java/org/oppia/app/profile/AdminAuthFragment.kt +++ b/app/src/main/java/org/oppia/app/profile/AdminAuthFragment.kt @@ -12,7 +12,7 @@ import javax.inject.Inject class AdminAuthFragment : InjectableFragment() { @Inject lateinit var adminAuthFragmentPresenter: AdminAuthFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/profile/AdminAuthFragmentPresenter.kt b/app/src/main/java/org/oppia/app/profile/AdminAuthFragmentPresenter.kt index fb6b6d6986b..d34e2c3fd1d 100644 --- a/app/src/main/java/org/oppia/app/profile/AdminAuthFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/profile/AdminAuthFragmentPresenter.kt @@ -1,6 +1,5 @@ package org.oppia.app.profile -import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/org/oppia/app/profile/ProfileChooserFragment.kt b/app/src/main/java/org/oppia/app/profile/ProfileChooserFragment.kt index c87e270732f..de45bff3918 100644 --- a/app/src/main/java/org/oppia/app/profile/ProfileChooserFragment.kt +++ b/app/src/main/java/org/oppia/app/profile/ProfileChooserFragment.kt @@ -12,7 +12,7 @@ import javax.inject.Inject class ProfileChooserFragment : InjectableFragment() { @Inject lateinit var profileChooserFragmentPresenter: ProfileChooserFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt b/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt index fe7736607eb..89ae3538fae 100644 --- a/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/profile/ProfileChooserFragmentPresenter.kt @@ -1,6 +1,5 @@ package org.oppia.app.profile -import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/org/oppia/app/profile/ProfileChooserViewModel.kt b/app/src/main/java/org/oppia/app/profile/ProfileChooserViewModel.kt index 22bbfc5dc43..c80c6ae4e17 100644 --- a/app/src/main/java/org/oppia/app/profile/ProfileChooserViewModel.kt +++ b/app/src/main/java/org/oppia/app/profile/ProfileChooserViewModel.kt @@ -1,6 +1,5 @@ package org.oppia.app.profile -import androidx.lifecycle.ViewModel import org.oppia.app.fragment.FragmentScope import org.oppia.app.viewmodel.ObservableViewModel import javax.inject.Inject diff --git a/app/src/main/java/org/oppia/app/recyclerview/BindableAdapter.kt b/app/src/main/java/org/oppia/app/recyclerview/BindableAdapter.kt index 284cecea02e..858f4032a2b 100644 --- a/app/src/main/java/org/oppia/app/recyclerview/BindableAdapter.kt +++ b/app/src/main/java/org/oppia/app/recyclerview/BindableAdapter.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.ViewGroup import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView +import org.oppia.app.recyclerview.BindableAdapter.Builder.Companion.newBuilder import kotlin.reflect.KClass /** A function that returns the type of view that can bind the specified data object. */ diff --git a/app/src/main/java/org/oppia/app/recyclerview/RecyclerViewBindingAdapter.kt b/app/src/main/java/org/oppia/app/recyclerview/RecyclerViewBindingAdapter.kt index 8eb836fd255..b3dea562365 100644 --- a/app/src/main/java/org/oppia/app/recyclerview/RecyclerViewBindingAdapter.kt +++ b/app/src/main/java/org/oppia/app/recyclerview/RecyclerViewBindingAdapter.kt @@ -1,6 +1,7 @@ package org.oppia.app.recyclerview import androidx.databinding.BindingAdapter +import androidx.databinding.ObservableList import androidx.lifecycle.LiveData import androidx.recyclerview.widget.RecyclerView @@ -10,11 +11,21 @@ import androidx.recyclerview.widget.RecyclerView * https://android.jlelse.eu/1bd08b4796b4. */ @BindingAdapter("data") -fun bindToRecyclerViewAdapter(recyclerView: RecyclerView, liveData: LiveData>) { +fun bindToRecyclerViewAdapterWithLiveData(recyclerView: RecyclerView, liveData: LiveData>) { liveData.value?.let { data -> - val adapter = recyclerView.adapter - checkNotNull(adapter) { "Cannot bind data to a RecyclerView missing its adapter." } - check(adapter is BindableAdapter<*>) { "Can only bind data to a BindableAdapter." } - adapter.setDataUnchecked(data) + bindToRecyclerViewAdapter(recyclerView, data) } } + +/** A variant of [bindToRecyclerViewAdapterWithLiveData] that instead uses an observable list. */ +@BindingAdapter("data") +fun bindToRecyclerViewAdapterWithObservableList(recyclerView: RecyclerView, dataList: ObservableList) { + bindToRecyclerViewAdapter(recyclerView, dataList) +} + +private fun bindToRecyclerViewAdapter(recyclerView: RecyclerView, dataList: List) { + val adapter = recyclerView.adapter + checkNotNull(adapter) { "Cannot bind data to a RecyclerView missing its adapter." } + check(adapter is BindableAdapter<*>) { "Can only bind data to a BindableAdapter." } + adapter.setDataUnchecked(dataList) +} diff --git a/app/src/main/java/org/oppia/app/story/StoryFragment.kt b/app/src/main/java/org/oppia/app/story/StoryFragment.kt index f5e88b9a3c4..13b5d6be25c 100644 --- a/app/src/main/java/org/oppia/app/story/StoryFragment.kt +++ b/app/src/main/java/org/oppia/app/story/StoryFragment.kt @@ -26,7 +26,7 @@ class StoryFragment : InjectableFragment(), ExplorationSelectionListener { @Inject lateinit var storyFragmentPresenter: StoryFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/testing/BindableAdapterTestFragment.kt b/app/src/main/java/org/oppia/app/testing/BindableAdapterTestFragment.kt index 4876e16dbd0..0a9c477dc1a 100644 --- a/app/src/main/java/org/oppia/app/testing/BindableAdapterTestFragment.kt +++ b/app/src/main/java/org/oppia/app/testing/BindableAdapterTestFragment.kt @@ -16,7 +16,7 @@ class BindableAdapterTestFragment: InjectableFragment() { @Inject lateinit var bindableAdapterTestFragmentPresenter: BindableAdapterTestFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/testing/HtmlParserTestActivity.kt b/app/src/main/java/org/oppia/app/testing/HtmlParserTestActivity.kt index a714468de15..af83ac604af 100644 --- a/app/src/main/java/org/oppia/app/testing/HtmlParserTestActivity.kt +++ b/app/src/main/java/org/oppia/app/testing/HtmlParserTestActivity.kt @@ -22,7 +22,7 @@ class HtmlParserTestActivity : InjectableAppCompatActivity() { val rawDummyString = "\u003cp\u003e\"Let's try one last question,\" said Mr. Baker. \"Here's a pineapple cake cut into pieces.\"\u003c/p\u003e\u003coppia-noninteractive-image alt-with-value=\"\u0026amp;quot;Pineapple cake with 7/9 having cherries.\u0026amp;quot;\" caption-with-value=\"\u0026amp;quot;\u0026amp;quot;\" filepath-with-value=\"\u0026amp;quot;pineapple_cake_height_479_width_480.png\u0026amp;quot;\"\u003e\u003c/oppia-noninteractive-image\u003e\u003cp\u003e\u00a0\u003c/p\u003e\u003cp\u003e\u003cstrong\u003eQuestion 6\u003c/strong\u003e: What fraction of the cake has big red cherries in the pineapple slices?\u003c/p\u003e" val htmlResult: Spannable = - htmlParserFactory.create( /* entityType= */ "exploration", /* entityId= */ "oppia-welcome") + htmlParserFactory.create( /* entityType= */ "exploration", /* entityId= */ "oppia") .parseOppiaHtml( rawDummyString, testHtmlContentTextView diff --git a/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt index daac9452205..749dbdabb3e 100644 --- a/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt +++ b/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt @@ -1,19 +1,30 @@ package org.oppia.app.testing -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil import org.oppia.app.R +import org.oppia.app.customview.interaction.FractionInputInteractionView import org.oppia.app.customview.interaction.NumericInputInteractionView import org.oppia.app.customview.interaction.TextInputInteractionView -import org.oppia.app.customview.interaction.FractionInputInteractionView +import org.oppia.app.databinding.ActivityNumericInputInteractionViewTestBinding +import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.itemviewmodel.NumericInputViewModel /** * This is a dummy activity to test input interaction views. - * It contains [NumericInputInteractionView], [TextInputInteractionView] and [FractionInputInteractionView]. + * It contains [NumericInputInteractionView], [TextInputInteractionView], [FractionInputInteractionView] and [NumberWithUnitsInputInteractionView]. */ class InputInteractionViewTestActivity : AppCompatActivity() { + val numericInputViewModel = NumericInputViewModel( + existingAnswer = InteractionObject.getDefaultInstance(), isReadOnly = false + ) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_numeric_input_interaction_view_test) + val binding = DataBindingUtil.setContentView( + this, R.layout.activity_numeric_input_interaction_view_test + ) + binding.numericInputViewModel = numericInputViewModel } } diff --git a/app/src/main/java/org/oppia/app/topic/TopicFragment.kt b/app/src/main/java/org/oppia/app/topic/TopicFragment.kt index 81eaae69971..d4a1e9e71a3 100644 --- a/app/src/main/java/org/oppia/app/topic/TopicFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/TopicFragment.kt @@ -15,7 +15,7 @@ class TopicFragment : InjectableFragment() { lateinit var topicFragmentPresenter: TopicFragmentPresenter lateinit var topicId: String - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt b/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt index c1590a2e940..126e35c418a 100644 --- a/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt +++ b/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt @@ -13,7 +13,7 @@ import org.oppia.app.topic.train.TopicTrainFragment class ViewPagerAdapter(fragmentManager: FragmentManager, private val topicId: String) : FragmentStatePagerAdapter(fragmentManager) { - override fun getItem(position: Int): Fragment? { + override fun getItem(position: Int): Fragment { val args = Bundle() args.putString(TOPIC_ID_ARGUMENT_KEY, topicId) when (TopicTab.getTabForPosition(position)) { diff --git a/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragment.kt b/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragment.kt index a9c7b5f4c41..a47cdddcc0d 100644 --- a/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragment.kt @@ -31,7 +31,7 @@ class ConceptCardFragment : InjectableDialogFragment() { @Inject lateinit var conceptCardPresenter: ConceptCardPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/topic/overview/TopicOverviewFragment.kt b/app/src/main/java/org/oppia/app/topic/overview/TopicOverviewFragment.kt index 589b41f92d0..f98e041bba5 100644 --- a/app/src/main/java/org/oppia/app/topic/overview/TopicOverviewFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/overview/TopicOverviewFragment.kt @@ -13,7 +13,7 @@ class TopicOverviewFragment : InjectableFragment() { @Inject lateinit var topicOverviewFragmentPresenter: TopicOverviewFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/topic/play/ChapterSummaryAdapter.kt b/app/src/main/java/org/oppia/app/topic/play/ChapterSummaryAdapter.kt index 31b606bfb3f..c9f1322da9e 100755 --- a/app/src/main/java/org/oppia/app/topic/play/ChapterSummaryAdapter.kt +++ b/app/src/main/java/org/oppia/app/topic/play/ChapterSummaryAdapter.kt @@ -1,10 +1,10 @@ package org.oppia.app.topic.play +import android.view.LayoutInflater import android.view.ViewGroup +import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.RecyclerView import org.oppia.app.R -import androidx.databinding.DataBindingUtil -import android.view.LayoutInflater import org.oppia.app.databinding.PlayChapterViewBinding import org.oppia.app.model.ChapterSummary diff --git a/app/src/main/java/org/oppia/app/topic/play/TopicPlayFragment.kt b/app/src/main/java/org/oppia/app/topic/play/TopicPlayFragment.kt index 3b9be343d53..e68d4d8e7ed 100644 --- a/app/src/main/java/org/oppia/app/topic/play/TopicPlayFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/play/TopicPlayFragment.kt @@ -17,7 +17,7 @@ class TopicPlayFragment : InjectableFragment(), ExpandedChapterListIndexListener private var currentExpandedChapterListIndex: Int? = null - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragment.kt b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragment.kt index c3b2ce22195..fa15d4c4fd0 100644 --- a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragment.kt @@ -13,7 +13,7 @@ class QuestionPlayerFragment: InjectableFragment(){ @Inject lateinit var questionPlayerFragmentPresenter: QuestionPlayerFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/topic/review/ReviewSkillSelectionAdapter.kt b/app/src/main/java/org/oppia/app/topic/review/ReviewSkillSelectionAdapter.kt index 315a01edea6..29404ebf9e7 100644 --- a/app/src/main/java/org/oppia/app/topic/review/ReviewSkillSelectionAdapter.kt +++ b/app/src/main/java/org/oppia/app/topic/review/ReviewSkillSelectionAdapter.kt @@ -5,9 +5,9 @@ import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.databinding.library.baseAdapters.BR import androidx.recyclerview.widget.RecyclerView +import org.oppia.app.R import org.oppia.app.databinding.TopicReviewSummaryViewBinding import org.oppia.app.model.SkillSummary -import org.oppia.app.R // TODO(#216): Make use of generic data-binding-enabled RecyclerView adapter. /** Adapter to bind skills to [RecyclerView] inside [TopicReviewFragment]. */ diff --git a/app/src/main/java/org/oppia/app/topic/review/TopicReviewFragment.kt b/app/src/main/java/org/oppia/app/topic/review/TopicReviewFragment.kt index 538e90003a1..de34f33d933 100644 --- a/app/src/main/java/org/oppia/app/topic/review/TopicReviewFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/review/TopicReviewFragment.kt @@ -14,7 +14,7 @@ class TopicReviewFragment : InjectableFragment(), ReviewSkillSelector { @Inject lateinit var topicReviewFragmentPresenter: TopicReviewFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragment.kt b/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragment.kt index dde27acf032..c643ac74695 100644 --- a/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragment.kt @@ -15,7 +15,7 @@ class TopicTrainFragment : InjectableFragment() { @Inject lateinit var topicTrainFragmentPresenter: TopicTrainFragmentPresenter - override fun onAttach(context: Context?) { + override fun onAttach(context: Context) { super.onAttach(context) fragmentComponent.inject(this) } diff --git a/app/src/main/java/org/oppia/app/view/ViewComponent.kt b/app/src/main/java/org/oppia/app/view/ViewComponent.kt new file mode 100644 index 00000000000..3985812066b --- /dev/null +++ b/app/src/main/java/org/oppia/app/view/ViewComponent.kt @@ -0,0 +1,21 @@ +package org.oppia.app.view + +import android.view.View +import dagger.BindsInstance +import dagger.Subcomponent +import org.oppia.app.player.state.SelectionInteractionView + +/** Root subcomponent for custom views. */ +@Subcomponent +@ViewScope +interface ViewComponent { + @Subcomponent.Builder + interface Builder { + @BindsInstance + fun setView(view: View): Builder + + fun build(): ViewComponent + } + + fun inject(selectionInteractionView: SelectionInteractionView) +} diff --git a/app/src/main/java/org/oppia/app/view/ViewScope.kt b/app/src/main/java/org/oppia/app/view/ViewScope.kt new file mode 100644 index 00000000000..de9b4bd348b --- /dev/null +++ b/app/src/main/java/org/oppia/app/view/ViewScope.kt @@ -0,0 +1,6 @@ +package org.oppia.app.view + +import javax.inject.Scope + +/** A custom scope corresponding to dependencies that should be recreated for each view. */ +@Scope annotation class ViewScope diff --git a/app/src/main/res/drawable/continue_button_answer_background.xml b/app/src/main/res/drawable/continue_button_answer_background.xml new file mode 100644 index 00000000000..ac93d6859f5 --- /dev/null +++ b/app/src/main/res/drawable/continue_button_answer_background.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_volume_off_48dp.xml b/app/src/main/res/drawable/ic_volume_off_48dp.xml new file mode 100644 index 00000000000..1f1aae9e628 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_off_48dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/layout/activity_numeric_input_interaction_view_test.xml b/app/src/main/res/layout/activity_numeric_input_interaction_view_test.xml index 807f98d0bb4..fd72b7db36b 100644 --- a/app/src/main/res/layout/activity_numeric_input_interaction_view_test.xml +++ b/app/src/main/res/layout/activity_numeric_input_interaction_view_test.xml @@ -1,51 +1,65 @@ - + - - - + + + + + - + android:layout_height="match_parent" + android:gravity="center" + android:orientation="vertical" + tools:context=".testing.InputInteractionViewTestActivity"> + + + + + + + + diff --git a/app/src/main/res/layout/add_profile_fragment.xml b/app/src/main/res/layout/add_profile_fragment.xml index acae5072cd2..7e42279a97a 100644 --- a/app/src/main/res/layout/add_profile_fragment.xml +++ b/app/src/main/res/layout/add_profile_fragment.xml @@ -1,8 +1,7 @@ - + + - + android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/admin_auth_fragment.xml b/app/src/main/res/layout/admin_auth_fragment.xml index 8bcad6e3107..7e42279a97a 100644 --- a/app/src/main/res/layout/admin_auth_fragment.xml +++ b/app/src/main/res/layout/admin_auth_fragment.xml @@ -1,8 +1,7 @@ - + + - + android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/audio_fragment.xml b/app/src/main/res/layout/audio_fragment.xml index 3ad1a24fe9c..fce30a0f2b9 100755 --- a/app/src/main/res/layout/audio_fragment.xml +++ b/app/src/main/res/layout/audio_fragment.xml @@ -1,15 +1,20 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + - + + + + type="org.oppia.app.player.audio.AudioFragment" /> + + type="org.oppia.app.player.audio.AudioViewModel" /> + + + app:layout_constraintTop_toTopOf="parent" /> + + app:layout_constraintTop_toTopOf="parent" /> + + app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/audio_fragment_test_activity.xml b/app/src/main/res/layout/audio_fragment_test_activity.xml index 63a71bf40e3..bcab8fe29a9 100644 --- a/app/src/main/res/layout/audio_fragment_test_activity.xml +++ b/app/src/main/res/layout/audio_fragment_test_activity.xml @@ -1,6 +1,5 @@ - - + - + android:text="@string/cellular_data_alert_dialog_checkbox"> diff --git a/app/src/main/res/layout/concept_card_example_view.xml b/app/src/main/res/layout/concept_card_example_view.xml index a903c7e0c76..89f420b19ef 100644 --- a/app/src/main/res/layout/concept_card_example_view.xml +++ b/app/src/main/res/layout/concept_card_example_view.xml @@ -1,15 +1,18 @@ - + + + + + android:text="@{subtitledHtml.getHtml()}" /> diff --git a/app/src/main/res/layout/concept_card_fragment_test_activity.xml b/app/src/main/res/layout/concept_card_fragment_test_activity.xml index 8505adfd9d1..df200985f2f 100644 --- a/app/src/main/res/layout/concept_card_fragment_test_activity.xml +++ b/app/src/main/res/layout/concept_card_fragment_test_activity.xml @@ -1,12 +1,13 @@ - +