diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index dcb6b8c4cc2..00000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/WORKSPACE b/WORKSPACE index 157e4c48167..b7e0ad81dd8 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -104,6 +104,17 @@ git_repository( load("@tools_android//tools/googleservices:defs.bzl", "google_services_workspace_dependencies") google_services_workspace_dependencies() +git_repository( + name = "circularimageview", + commit = "6098dec76713b34eb8b10883cbe54189ddc08566", + remote = "https://github.com/oppia/CircularImageview", +) + +bind( + name = "databinding_annotation_processor", + actual = "//tools/android:compiler_annotation_processor", +) + load("@rules_jvm_external//:defs.bzl", "maven_install") maven_install( @@ -112,27 +123,47 @@ maven_install( "androidx.annotation:annotation:1.1.0", "androidx.appcompat:appcompat:1.0.2", "androidx.arch.core:core-testing:2.1.0", + "androidx.constraintlayout:constraintlayout:1.1.3", "androidx.core:core-ktx:1.0.1", - "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03", + "androidx.core:core:1.0.1", + "androidx.databinding:databinding-adapters:3.4.2", + "androidx.databinding:databinding-common:3.4.2", + "androidx.databinding:databinding-runtime:3.4.2", + "androidx.lifecycle:lifecycle-extensions:2.2.0", + "androidx.lifecycle:lifecycle-livedata-core:2.2.0", + "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0", + "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0", + "androidx.multidex:multidex-instrumentation:2.0.0", + "androidx.multidex:multidex:2.0.1", + "androidx.recyclerview:recyclerview:1.0.0", "androidx.test.espresso:espresso-contrib:3.1.0", "androidx.test.espresso:espresso-core:3.2.0", "androidx.test.espresso:espresso-intents:3.1.0", "androidx.test.ext:junit:1.1.1", "androidx.test:runner:1.2.0", + "androidx.viewpager:viewpager:1.0.0", "com.android.support:support-annotations:28.0.0", "com.caverock:androidsvg-aar:1.4", + "com.chaos.view:pinview:1.4.3", "com.crashlytics.sdk.android:crashlytics:2.9.8", "com.github.bumptech.glide:glide:4.11.0", + "com.google.android.material:material:1.2.0-alpha02", "com.google.firebase:firebase-analytics:17.4.4", "com.google.firebase:firebase-crashlytics:17.1.1", + "com.google.gms:google-services:4.3.3", "com.google.truth:truth:0.43", + "com.squareup.retrofit2:converter-gson:2.5.0", + "com.squareup.retrofit2:retrofit:2.9.0", + "de.hdodenhof:circleimageview:3.0.1", "io.fabric.sdk.android:fabric:1.4.7", + "javax.annotation:javax.annotation-api:jar:1.3.2", "junit:junit:4.12", "org.jetbrains.kotlin:kotlin-reflect:1.3.41", "org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.3.72", "org.jetbrains.kotlin:kotlin-test-junit:1.3.72", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2", "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2", + "org.jetbrains:annotations:jar:13.0", "org.mockito:mockito-core:2.19.0", "org.robolectric:annotations:4.3", "org.robolectric:robolectric:4.3", diff --git a/app/BUILD.bazel b/app/BUILD.bazel index f1e3fcf6d51..5744f21ce76 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -1,17 +1,345 @@ # TODO(#1532): Rename file to 'BUILD' post-Gradle. ''' -Package for all Firebase dependencies. -To reference these dependencies, add '//app:crashlytics' and '//app:crashlytics_deps' -to your build rule's dependency list. +This library contains the app's core source files and functionality ''' load("@rules_jvm_external//:defs.bzl", "artifact") load("@dagger//:workspace_defs.bzl", "dagger_rules") load("@tools_android//tools/crashlytics:defs.bzl", "crashlytics_android_library") load("@tools_android//tools/googleservices:defs.bzl", "google_services_xml") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +# Source files for annotations library. +ANNOTATION_FILES = [ + "src/main/java/org/oppia/app/activity/ActivityScope.kt", + "src/main/java/org/oppia/app/fragment/FragmentScope.kt", + "src/main/java/org/oppia/app/utility/KeyboardHelper.kt", +] + +# Source files for listener library. +LISTENER_FILES = [ + "src/main/java/org/oppia/app/administratorcontrols/RouteToAppVersionListener.kt", + "src/main/java/org/oppia/app/administratorcontrols/RouteToProfileListListener.kt", + "src/main/java/org/oppia/app/drawer/RouteToProfileProgressListener.kt", + "src/main/java/org/oppia/app/help/faq/RouteToFAQSingleListener.kt", + "src/main/java/org/oppia/app/help/RouteToFAQListListener.kt", + "src/main/java/org/oppia/app/home/recentlyplayed/OngoingStoryClickListener.kt", + "src/main/java/org/oppia/app/home/RouteToRecentlyPlayedListener.kt", + "src/main/java/org/oppia/app/home/RouteToTopicPlayStoryListener.kt", + "src/main/java/org/oppia/app/home/topiclist/TopicSummaryClickListener.kt", + "src/main/java/org/oppia/app/onboarding/RouteToProfileListListener.kt", + "src/main/java/org/oppia/app/options/RouteToAppLanguageListListener.kt", + "src/main/java/org/oppia/app/options/RouteToAudioLanguageListListener.kt", + "src/main/java/org/oppia/app/options/RouteToStoryTextSizeListener.kt", + "src/main/java/org/oppia/app/player/audio/LanguageInterface.kt", + "src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerErrorOrAvailabilityCheckReceiver.kt", + "src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt", + "src/main/java/org/oppia/app/player/state/listener/ContinueNavigationButtonListener.kt", + "src/main/java/org/oppia/app/player/state/listener/NextNavigationButtonListener.kt", + "src/main/java/org/oppia/app/player/state/listener/PreviousNavigationButtonListener.kt", + "src/main/java/org/oppia/app/player/state/listener/PreviousResponsesHeaderClickListener.kt", + "src/main/java/org/oppia/app/player/state/listener/ReplayButtonListener.kt", + "src/main/java/org/oppia/app/player/state/listener/ReturnToTopicNavigationButtonListener.kt", + "src/main/java/org/oppia/app/player/state/listener/RouteToHintsAndSolutionListener.kt", + "src/main/java/org/oppia/app/player/state/listener/StateKeyboardButtonListener.kt", + "src/main/java/org/oppia/app/player/state/listener/SubmitNavigationButtonListener.kt", + "src/main/java/org/oppia/app/profile/RouteToAdminPinListener.kt", + "src/main/java/org/oppia/app/profileprogress/ProfilePictureClickListener.kt", + "src/main/java/org/oppia/app/profileprogress/RouteToCompletedStoryListListener.kt", + "src/main/java/org/oppia/app/profileprogress/RouteToOngoingTopicListListener.kt", + "src/main/java/org/oppia/app/recyclerview/OnDragEndedListener.kt", + "src/main/java/org/oppia/app/recyclerview/OnItemDragListener.kt", + "src/main/java/org/oppia/app/story/ExplorationSelectionListener.kt", + "src/main/java/org/oppia/app/topic/lessons/StorySummarySelector.kt", + "src/main/java/org/oppia/app/topic/revision/RevisionSubtopicSelector.kt", + "src/main/java/org/oppia/app/topic/revisioncard/ReturnToTopicClickListener.kt", + "src/main/java/org/oppia/app/topic/RouteToRevisionCardListener.kt", + "src/main/java/org/oppia/app/utility/OnClickableAreaClickedListener.kt", + "src/main/java/org/oppia/app/utility/RegionClickEvent.kt", +] + +# TODO(#1617): Remove genrules post-gradle +# Files altered by genrules to be excluded in the app library +ALTERED_VIEW_MODELS = [ + "src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsAccountActionsViewModel.kt", + "src/main/java/org/oppia/app/help/faq/FAQListViewModel.kt", + "src/main/java/org/oppia/app/help/HelpItemViewModel.kt", + "src/main/java/org/oppia/app/help/HelpListViewModel.kt", + "src/main/java/org/oppia/app/onboarding/OnboadingSlideViewModel.kt", + "src/main/java/org/oppia/app/onboarding/OnboardingViewModel.kt", + "src/main/java/org/oppia/app/parser/StringToFractionParser.kt", + "src/main/java/org/oppia/app/parser/StringToNumberParser.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt", + "src/main/java/org/oppia/app/topic/info/TopicInfoViewModel.kt", +] + +# Source files for view_model library +VIEW_MODEL_FILES = [ + "src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsAppInformationViewModel.kt", + "src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt", + "src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsGeneralViewModel.kt", + "src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsItemViewModel.kt", + "src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsProfileViewModel.kt", + "src/main/java/org/oppia/app/administratorcontrols/AdministratorControlsViewModel.kt", + "src/main/java/org/oppia/app/administratorcontrols/appversion/AppVersionViewModel.kt", + "src/main/java/org/oppia/app/completedstorylist/CompletedStoryItemViewModel.kt", + "src/main/java/org/oppia/app/completedstorylist/CompletedStoryListViewModel.kt", + "src/main/java/org/oppia/app/drawer/NavigationDrawerFooterViewModel.kt", + "src/main/java/org/oppia/app/drawer/NavigationDrawerHeaderViewModel.kt", + "src/main/java/org/oppia/app/help/faq/faqItemViewModel/FAQContentViewModel.kt", + "src/main/java/org/oppia/app/help/faq/faqItemViewModel/FAQHeaderViewModel.kt", + "src/main/java/org/oppia/app/help/faq/faqItemViewModel/FAQItemViewModel.kt", + "src/main/java/org/oppia/app/help/HelpItems.kt", + "src/main/java/org/oppia/app/hintsandsolution/HintsAndSolutionItemViewModel.kt", + "src/main/java/org/oppia/app/hintsandsolution/HintsViewModel.kt", + "src/main/java/org/oppia/app/hintsandsolution/SolutionViewModel.kt", + "src/main/java/org/oppia/app/home/HomeItemViewModel.kt", + "src/main/java/org/oppia/app/home/recentlyplayed/OngoingStoryViewModel.kt", + "src/main/java/org/oppia/app/home/recentlyplayed/RecentlyPlayedItemViewModel.kt", + "src/main/java/org/oppia/app/home/recentlyplayed/SectionTitleViewModel.kt", + "src/main/java/org/oppia/app/home/topiclist/AllTopicsViewModel.kt", + "src/main/java/org/oppia/app/home/topiclist/PromotedStoryListViewModel.kt", + "src/main/java/org/oppia/app/home/topiclist/PromotedStoryViewModel.kt", + "src/main/java/org/oppia/app/home/topiclist/TopicSummaryViewModel.kt", + "src/main/java/org/oppia/app/home/UserAppHistoryViewModel.kt", + "src/main/java/org/oppia/app/home/WelcomeViewModel.kt", + "src/main/java/org/oppia/app/onboarding/OnboardingSlideFinalViewModel.kt", + "src/main/java/org/oppia/app/onboarding/ViewPagerSlide.kt", + "src/main/java/org/oppia/app/ongoingtopiclist/OngoingTopicItemViewModel.kt", + "src/main/java/org/oppia/app/ongoingtopiclist/OngoingTopicListViewModel.kt", + "src/main/java/org/oppia/app/options/OptionControlsViewModel.kt", + "src/main/java/org/oppia/app/options/OptionsAppLanguageViewModel.kt", + "src/main/java/org/oppia/app/options/OptionsAudioLanguageViewModel.kt", + "src/main/java/org/oppia/app/options/OptionsItemViewModel.kt", + "src/main/java/org/oppia/app/options/OptionsStoryTextSizeViewModel.kt", + "src/main/java/org/oppia/app/player/audio/AudioViewModel.kt", + "src/main/java/org/oppia/app/player/exploration/ExplorationViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/ContinueNavigationButtonViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/FeedbackViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/NextButtonViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/NumericInputViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/PreviousButtonViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/PreviousResponsesHeaderViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/ReplayButtonViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/ReturnToTopicButtonViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionContentViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/StateItemViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/SubmitButtonViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/SubmittedAnswerViewModel.kt", + "src/main/java/org/oppia/app/player/state/itemviewmodel/TextInputViewModel.kt", + "src/main/java/org/oppia/app/player/state/StateViewModel.kt", + "src/main/java/org/oppia/app/player/state/testing/StateFragmentTestViewModel.kt", + "src/main/java/org/oppia/app/profile/AddProfileViewModel.kt", + "src/main/java/org/oppia/app/profile/AdminAuthViewModel.kt", + "src/main/java/org/oppia/app/profile/AdminPinViewModel.kt", + "src/main/java/org/oppia/app/profile/AdminSettingsViewModel.kt", + "src/main/java/org/oppia/app/profile/PinPasswordViewModel.kt", + "src/main/java/org/oppia/app/profile/ProfileChooserViewModel.kt", + "src/main/java/org/oppia/app/profile/ResetPinViewModel.kt", + "src/main/java/org/oppia/app/profileprogress/ProfilePictureActivityViewModel.kt", + "src/main/java/org/oppia/app/profileprogress/ProfileProgressHeaderViewModel.kt", + "src/main/java/org/oppia/app/profileprogress/ProfileProgressItemViewModel.kt", + "src/main/java/org/oppia/app/profileprogress/ProfileProgressViewModel.kt", + "src/main/java/org/oppia/app/profileprogress/RecentlyPlayedStorySummaryViewModel.kt", + "src/main/java/org/oppia/app/recyclerview/BindableAdapter.kt", + "src/main/java/org/oppia/app/recyclerview/DividerItemDecorator.kt", + "src/main/java/org/oppia/app/recyclerview/DragAndDropItemFacilitator.kt", + "src/main/java/org/oppia/app/settings/profile/ProfileEditViewModel.kt", + "src/main/java/org/oppia/app/settings/profile/ProfileListViewModel.kt", + "src/main/java/org/oppia/app/settings/profile/ProfileRenameViewModel.kt", + "src/main/java/org/oppia/app/settings/profile/ProfileResetPinViewModel.kt", + "src/main/java/org/oppia/app/shim/IntentFactoryShim.kt", + "src/main/java/org/oppia/app/story/StoryFragmentScroller.kt", + "src/main/java/org/oppia/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt", + "src/main/java/org/oppia/app/story/storyitemviewmodel/StoryHeaderViewModel.kt", + "src/main/java/org/oppia/app/story/storyitemviewmodel/StoryItemViewModel.kt", + "src/main/java/org/oppia/app/story/StoryViewModel.kt", + "src/main/java/org/oppia/app/testing/BindableAdapterTestViewModel.kt", + "src/main/java/org/oppia/app/topic/conceptcard/ConceptCardViewModel.kt", + "src/main/java/org/oppia/app/topic/lessons/StorySummaryViewModel.kt", + "src/main/java/org/oppia/app/topic/lessons/TopicLessonsItemViewModel.kt", + "src/main/java/org/oppia/app/topic/lessons/TopicLessonsTitleViewModel.kt", + "src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeFooterViewModel.kt", + "src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeHeaderViewModel.kt", + "src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeItemViewModel.kt", + "src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt", + "src/main/java/org/oppia/app/topic/practice/TopicPracticeViewModel.kt", + "src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerViewModel.kt", + "src/main/java/org/oppia/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt", + "src/main/java/org/oppia/app/topic/revision/TopicRevisionViewModel.kt", + "src/main/java/org/oppia/app/topic/revisioncard/RevisionCardViewModel.kt", + "src/main/java/org/oppia/app/topic/TopicViewModel.kt", + "src/main/java/org/oppia/app/viewmodel/ObservableArrayList.kt", + "src/main/java/org/oppia/app/viewmodel/ObservableViewModel.kt", + "src/main/java/org/oppia/app/walkthrough/end/WalkthroughFinalViewModel.kt", + "src/main/java/org/oppia/app/walkthrough/topiclist/topiclistviewmodel/WalkthroughTopicHeaderViewModel.kt", + "src/main/java/org/oppia/app/walkthrough/topiclist/topiclistviewmodel/WalkthroughTopicSummaryViewModel.kt", + "src/main/java/org/oppia/app/walkthrough/topiclist/WalkthroughTopicItemViewModel.kt", + "src/main/java/org/oppia/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt", + "src/main/java/org/oppia/app/walkthrough/WalkthroughViewModel.kt", + "src/main/java/org/oppia/app/walkthrough/welcome/WalkthroughWelcomeViewModel.kt", +] + [ + "update_" + altered_view_models[0:-3] + for altered_view_models in ALTERED_VIEW_MODELS +] + +# TODO(#1617): Remove genrules post-gradle +''' +Genrule for source files in the view_models library. + +Because each databinding library must have a unique package name and manifest, resources must be +imported using the proper package name when building with Bazel. This genrule alters those imports +in order to keep Gradle building. +''' +[genrule( + name = "update_" + file[0:-3], + srcs = [file], + outs = [file[0:-3] + "_updated.kt"], + cmd = ''' + cat $(SRCS) | + sed 's/import org.oppia.app.R/import org.oppia.app.vm.R/g' > $(OUTS) + ''', +) +for file in ALTERED_VIEW_MODELS] + +# TODO(#1617): Remove genrules post-gradle +# View files altered by genrules to be excluded in the app library. +ALTERED_VIEWS = [ + "src/main/java/org/oppia/app/customview/LessonThumbnailImageView.kt", + "src/main/java/org/oppia/app/customview/SegmentedCircularProgressView.kt", + "src/main/java/org/oppia/app/profile/ProfileInputView.kt", + "src/main/java/org/oppia/app/utility/ClickableAreasImage.kt", +] + +# Source files and genrule targets for views library. +VIEW_FILES = [ + "src/main/java/org/oppia/app/customview/interaction/FractionInputInteractionView.kt", + "src/main/java/org/oppia/app/customview/interaction/NumericInputInteractionView.kt", + "src/main/java/org/oppia/app/customview/interaction/TextInputInteractionView.kt", + "src/main/java/org/oppia/app/player/state/DragDropSortInteractionView.kt", + "src/main/java/org/oppia/app/player/state/ImageRegionSelectionInteractionView.kt", + "src/main/java/org/oppia/app/player/state/SelectionInteractionView.kt", + "src/main/java/org/oppia/app/shim/ViewBindingShim.kt", + "src/main/java/org/oppia/app/shim/ViewComponentFactory.kt", + "src/main/java/org/oppia/app/view/ViewComponent.kt", + "src/main/java/org/oppia/app/view/ViewScope.kt", +] + [ + "update_" + altered_views[0:-3] + for altered_views in ALTERED_VIEWS +] + +# TODO(#1617): Remove genrules post-gradle +''' +Genrule for source files in the views library. + +Because each databinding library must have a unique package name and manifest, resources must be +imported using the proper package name when building with Bazel. This genrule alters those imports +in order to keep Gradle building. +''' +[genrule( + name = "update_" + file[0:-3], + srcs = [file], + outs = [file[0:-3] + "_updated.kt"], + cmd = ''' + cat $(SRCS) | + sed 's/import org.oppia.app.R/import org.oppia.app.views.R/g' > $(OUTS) + ''', + ) +for file in ALTERED_VIEWS] + +# Library for non-layout resource files +android_library( + name = "resources", + custom_package = "org.oppia.app", + manifest = "src/main/AndroidManifest.xml", + resource_files = glob(["src/main/res/**",], exclude = ["src/main/res/layout*/**"]), + exports_manifest = True, + deps = [ + artifact("com.google.android.material:material"), + ], + visibility = ["//visibility:private"], +) + +# Library for listener files required to build views and view_model libraries +kt_android_library( + name = "listeners", + custom_package = "org.oppia.app", + srcs = LISTENER_FILES, + deps = [ + ":dagger", + "//model", + artifact("androidx.recyclerview:recyclerview:1.0.0"), + ], + visibility = ["//visibility:private"], +) + +# Library for all view files required to build layout files +kt_android_library( + name = "views", + custom_package = "org.oppia.app.views", + manifest = "src/main/ViewsManifest.xml", + srcs = VIEW_FILES, + deps = [ + ":annotations", + ":listeners", + ":resources", + ":view_models", + "//model", + "@circularimageview//circularimageview:circular_image_view", + artifact("androidx.appcompat:appcompat"), + artifact("androidx.core:core-ktx"), + artifact("androidx.databinding:databinding-common"), + artifact("androidx.databinding:databinding-runtime"), + ], + visibility = ["//visibility:public"], +) + +# Library for scope annotations required to build views and view_model libraries +kt_android_library( + name = "annotations", + custom_package = "org.oppia.app", + srcs = ANNOTATION_FILES, + deps = [ + ":dagger", + "//model", + ], + visibility = ["//visibility:private"], +) +# Library for all view model files +kt_android_library( + name = "view_models", + custom_package = "org.oppia.app.vm", + srcs = VIEW_MODEL_FILES, + enable_data_binding = 1, + manifest = "src/main/ViewModelManifest.xml", + deps = [ + ":annotations", + ":dagger", + ":listeners", + ":resources", + "//domain", + "//model", + "//utility", + artifact("androidx.databinding:databinding-common"), + artifact("androidx.databinding:databinding-runtime"), + ], + visibility = ["//visibility:private"], +) + +# TODO(#1566): Move Firebase rules to their own package & remove default visibility +''' +Package for all Firebase dependencies. +To reference these dependencies, add '//app:crashlytics' and '//app:crashlytics_deps' +to your build rule's dependency list. +''' package(default_visibility = ["//visibility:public"]) -# TODO(#1566): Move Firebase rules to their own package GOOGLE_SERVICES_RESOURCES = google_services_xml( package_name = "org.oppia.app", google_services_json = "google-services.json", @@ -33,3 +361,5 @@ android_library( artifact("com.google.firebase:firebase-crashlytics"), ], ) + +dagger_rules() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6ddfa2bbb70..0a09ea7f726 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + + + + diff --git a/app/src/main/ViewsManifest.xml b/app/src/main/ViewsManifest.xml new file mode 100644 index 00000000000..0b89c5bf0fd --- /dev/null +++ b/app/src/main/ViewsManifest.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/java/org/oppia/app/administratorcontrols/AdministratorControlsViewModel.kt b/app/src/main/java/org/oppia/app/administratorcontrols/AdministratorControlsViewModel.kt index 659927f2e26..ef3862ab3c8 100644 --- a/app/src/main/java/org/oppia/app/administratorcontrols/AdministratorControlsViewModel.kt +++ b/app/src/main/java/org/oppia/app/administratorcontrols/AdministratorControlsViewModel.kt @@ -14,6 +14,7 @@ import org.oppia.app.administratorcontrols.administratorcontrolsitemviewmodel.Ad import org.oppia.app.fragment.FragmentScope import org.oppia.app.model.DeviceSettings import org.oppia.app.model.ProfileId +import org.oppia.app.shim.IntentFactoryShim import org.oppia.app.viewmodel.ObservableViewModel import org.oppia.domain.profile.ProfileManagementController import org.oppia.util.data.AsyncResult @@ -26,7 +27,8 @@ class AdministratorControlsViewModel @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val logger: ConsoleLogger, - private val profileManagementController: ProfileManagementController + private val profileManagementController: ProfileManagementController, + private val IntentFactoryShim: IntentFactoryShim ) : ObservableViewModel() { private val routeToProfileListListener = activity as RouteToProfileListListener private lateinit var userProfileId: ProfileId @@ -72,7 +74,12 @@ class AdministratorControlsViewModel @Inject constructor( ) ) itemViewModelList.add(AdministratorControlsAppInformationViewModel(activity)) - itemViewModelList.add(AdministratorControlsAccountActionsViewModel(fragment)) + itemViewModelList.add( + AdministratorControlsAccountActionsViewModel( + fragment, + IntentFactoryShim + ) + ) return itemViewModelList } diff --git a/app/src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsAccountActionsViewModel.kt b/app/src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsAccountActionsViewModel.kt index 2878ccfe8ef..805eb2b4821 100644 --- a/app/src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsAccountActionsViewModel.kt +++ b/app/src/main/java/org/oppia/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsAccountActionsViewModel.kt @@ -4,11 +4,12 @@ import android.content.Intent import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import org.oppia.app.R -import org.oppia.app.profile.ProfileActivity +import org.oppia.app.shim.IntentFactoryShim /** [ViewModel] for the recycler view in [AdministratorControlsFragment]. */ class AdministratorControlsAccountActionsViewModel( - private val fragment: Fragment + private val fragment: Fragment, + private val intentFactoryShim: IntentFactoryShim ) : AdministratorControlsItemViewModel() { fun onLogOutClicked() { @@ -19,7 +20,7 @@ class AdministratorControlsAccountActionsViewModel( } .setPositiveButton(R.string.log_out_dialog_okay_button) { _, _ -> // TODO(#762): Replace [ProfileChooserActivity] to [LoginActivity] once it is added. - val intent = Intent(fragment.activity, ProfileActivity::class.java) + val intent = intentFactoryShim.createProfileActivityIntent(fragment.activity!!) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) fragment.activity!!.startActivity(intent) fragment.activity!!.finish() diff --git a/app/src/main/java/org/oppia/app/administratorcontrols/appversion/AppVersionViewModel.kt b/app/src/main/java/org/oppia/app/administratorcontrols/appversion/AppVersionViewModel.kt index c78c71f6a25..78c32f1a782 100644 --- a/app/src/main/java/org/oppia/app/administratorcontrols/appversion/AppVersionViewModel.kt +++ b/app/src/main/java/org/oppia/app/administratorcontrols/appversion/AppVersionViewModel.kt @@ -1,8 +1,9 @@ package org.oppia.app.administratorcontrols.appversion +import android.content.Context import androidx.databinding.ObservableField import androidx.fragment.app.Fragment -import org.oppia.app.BuildConfig +import androidx.lifecycle.ViewModel import org.oppia.app.fragment.FragmentScope import org.oppia.app.viewmodel.ObservableViewModel import org.oppia.util.system.OppiaDateTimeFormatter @@ -13,10 +14,12 @@ import javax.inject.Inject @FragmentScope class AppVersionViewModel @Inject constructor( fragment: Fragment, - private val oppiaDateTimeFormatter: OppiaDateTimeFormatter + private val oppiaDateTimeFormatter: OppiaDateTimeFormatter, + context: Context ) : ObservableViewModel() { - val versionName = ObservableField(BuildConfig.VERSION_NAME) + val versionName: String = context.packageManager + .getPackageInfo(context.packageName, 0).versionName private val lastUpdateDateTime = fragment.activity!!.packageManager.getPackageInfo( diff --git a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt index 3a3038144e8..730a41438bc 100644 --- a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt @@ -4,6 +4,8 @@ import android.app.Application import dagger.BindsInstance import dagger.Component import org.oppia.app.activity.ActivityComponent +import org.oppia.app.shim.IntentFactoryShimModule +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.data.backends.gae.NetworkModule import org.oppia.domain.classify.InteractionsModule import org.oppia.domain.classify.rules.continueinteraction.ContinueModule @@ -45,7 +47,8 @@ import javax.inject.Singleton InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class, CachingModule::class, QuestionModule::class, LogReportingModule::class, AccessibilityModule::class, - ImageClickInputModule::class, LogStorageModule::class, PrimeTopicAssetsControllerModule::class, + ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class, + ViewBindingShimModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class ] ) diff --git a/app/src/main/java/org/oppia/app/customview/LessonThumbnailImageView.kt b/app/src/main/java/org/oppia/app/customview/LessonThumbnailImageView.kt index b21fc34a62a..a1bc7fc5f5d 100644 --- a/app/src/main/java/org/oppia/app/customview/LessonThumbnailImageView.kt +++ b/app/src/main/java/org/oppia/app/customview/LessonThumbnailImageView.kt @@ -3,11 +3,12 @@ package org.oppia.app.customview import android.content.Context import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import org.oppia.app.R -import org.oppia.app.fragment.InjectableFragment import org.oppia.app.model.LessonThumbnail import org.oppia.app.model.LessonThumbnailGraphic +import org.oppia.app.shim.ViewComponentFactory import org.oppia.util.gcsresource.DefaultResourceBucketName import org.oppia.util.parser.DefaultGcsPrefix import org.oppia.util.parser.ImageLoader @@ -98,9 +99,8 @@ class LessonThumbnailImageView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - FragmentManager.findFragment(this) - .createViewComponent(this) - .inject(this) + (FragmentManager.findFragment(this) as ViewComponentFactory) + .createViewComponent(this).inject(this) } private fun getLessonDrawableResource(lessonThumbnail: LessonThumbnail): Int { diff --git a/app/src/main/java/org/oppia/app/databinding/TextViewBindingAdapters.java b/app/src/main/java/org/oppia/app/databinding/TextViewBindingAdapters.java index 066010be643..b762b9f39c2 100644 --- a/app/src/main/java/org/oppia/app/databinding/TextViewBindingAdapters.java +++ b/app/src/main/java/org/oppia/app/databinding/TextViewBindingAdapters.java @@ -14,8 +14,6 @@ /** Holds all custom binding adapters that bind to [TextView]. */ public final class TextViewBindingAdapters { - private static int MINUTE_MILLIS = (int) TimeUnit.MINUTES.toMillis(1); - /** Binds date text with relative time. */ @BindingAdapter("profile:created") public static void setProfileDataText(@NonNull TextView textView, long timestamp) { @@ -57,7 +55,7 @@ private static String getTimeAgo(long lastVisitedTimeStamp, Context context) { Resources res = context.getResources(); long timeDifferenceMillis = currentTimeMillis - timeStampMillis; - if (timeDifferenceMillis < MINUTE_MILLIS) { + if (timeDifferenceMillis < (int) TimeUnit.MINUTES.toMillis(1)) { return context.getString(R.string.just_now); } else if (timeDifferenceMillis < TimeUnit.MINUTES.toMillis(50)) { return getPluralString( diff --git a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt index bdada11db7e..ff6294faa3f 100644 --- a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt +++ b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt @@ -32,6 +32,8 @@ import org.oppia.app.profile.AdminSettingsDialogFragment import org.oppia.app.profile.ProfileChooserFragment import org.oppia.app.profile.ResetPinDialogFragment import org.oppia.app.profileprogress.ProfileProgressFragment +import org.oppia.app.shim.IntentFactoryShimModule +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.story.StoryFragment import org.oppia.app.testing.BindableAdapterTestFragment import org.oppia.app.testing.ImageRegionSelectionTestFragment @@ -51,7 +53,12 @@ import org.oppia.app.walkthrough.welcome.WalkthroughWelcomeFragment import javax.inject.Provider /** Root subcomponent for all fragments. */ -@Subcomponent(modules = [FragmentModule::class, InteractionViewModelModule::class]) +@Subcomponent( + modules = [ + FragmentModule::class, InteractionViewModelModule::class, IntentFactoryShimModule::class, + ViewBindingShimModule::class + ] +) @FragmentScope interface FragmentComponent { @Subcomponent.Builder 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 cd8acf9f97b..f18a4541cdb 100644 --- a/app/src/main/java/org/oppia/app/fragment/InjectableFragment.kt +++ b/app/src/main/java/org/oppia/app/fragment/InjectableFragment.kt @@ -4,13 +4,14 @@ import android.content.Context import android.view.View import androidx.fragment.app.Fragment import org.oppia.app.activity.InjectableAppCompatActivity +import org.oppia.app.shim.ViewComponentFactory import org.oppia.app.view.ViewComponent /** * A fragment that facilitates field injection to children. This fragment can only be used with * [InjectableAppCompatActivity] contexts. */ -abstract class InjectableFragment : Fragment() { +abstract class InjectableFragment : Fragment(), ViewComponentFactory { /** * The [FragmentComponent] corresponding to this fragment. This cannot be used before [onAttach] is called, and can be * used to inject lateinit fields in child fragments during fragment attachment (which is recommended to be done in an @@ -24,7 +25,7 @@ abstract class InjectableFragment : Fragment() { (requireActivity() as InjectableAppCompatActivity).createFragmentComponent(this) } - fun createViewComponent(view: View): ViewComponent { + override fun createViewComponent(view: View): ViewComponent { return fragmentComponent.getViewComponentBuilderProvider().get().setView(view).build() } } 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 04724cc9c33..ce6f039b4f5 100644 --- a/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt @@ -25,6 +25,7 @@ import org.oppia.app.model.Profile import org.oppia.app.model.ProfileId import org.oppia.app.model.TopicList import org.oppia.app.model.TopicSummary +import org.oppia.app.shim.IntentFactoryShim import org.oppia.domain.oppialogger.OppiaLogger import org.oppia.domain.profile.ProfileManagementController import org.oppia.domain.topic.TopicListController @@ -46,6 +47,7 @@ class HomeFragmentPresenter @Inject constructor( private val oppiaClock: OppiaClock, private val logger: ConsoleLogger, private val oppiaLogger: OppiaLogger, + private val intentFactoryShim: IntentFactoryShim, @TopicHtmlParserEntityType private val topicEntityType: String, @StoryHtmlParserEntityType private val storyEntityType: String ) { @@ -71,7 +73,11 @@ class HomeFragmentPresenter @Inject constructor( logHomeActivityEvent() welcomeViewModel = WelcomeViewModel() - promotedStoryListViewModel = PromotedStoryListViewModel(activity, internalProfileId) + promotedStoryListViewModel = PromotedStoryListViewModel( + activity, + internalProfileId, + intentFactoryShim + ) allTopicsViewModel = AllTopicsViewModel() itemList.add(welcomeViewModel) itemList.add(promotedStoryListViewModel) @@ -191,14 +197,24 @@ class HomeFragmentPresenter @Inject constructor( promotedStoryList.clear() if (it.recentStoryCount != 0) { it.recentStoryList.take(limit).forEach { promotedStory -> - val recentStory = PromotedStoryViewModel(activity, internalProfileId, storyEntityType) + val recentStory = PromotedStoryViewModel( + activity, + internalProfileId, + storyEntityType, + intentFactoryShim + ) recentStory.setPromotedStory(promotedStory) promotedStoryList.add(recentStory) } } else { // TODO(#936): Optimise this as part of recommended stories. it.olderStoryList.take(limit).forEach { promotedStory -> - val oldStory = PromotedStoryViewModel(activity, internalProfileId, storyEntityType) + val oldStory = PromotedStoryViewModel( + activity, + internalProfileId, + storyEntityType, + intentFactoryShim + ) oldStory.setPromotedStory(promotedStory) promotedStoryList.add(oldStory) } diff --git a/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryListViewModel.kt b/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryListViewModel.kt index 84f039a17d1..135e69c2386 100644 --- a/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryListViewModel.kt +++ b/app/src/main/java/org/oppia/app/home/topiclist/PromotedStoryListViewModel.kt @@ -4,12 +4,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModel import org.oppia.app.home.HomeItemViewModel import org.oppia.app.home.RouteToRecentlyPlayedListener -import org.oppia.app.home.recentlyplayed.RecentlyPlayedActivity +import org.oppia.app.shim.IntentFactoryShim /** [ViewModel] promoted story list in [HomeFragment]. */ class PromotedStoryListViewModel( private val activity: AppCompatActivity, - private val internalProfileId: Int + private val internalProfileId: Int, + private val IntentFactoryShim: IntentFactoryShim ) : HomeItemViewModel(), RouteToRecentlyPlayedListener { @@ -19,11 +20,10 @@ class PromotedStoryListViewModel( } override fun routeToRecentlyPlayed() { - activity.startActivity( - RecentlyPlayedActivity.createRecentlyPlayedActivityIntent( - activity.applicationContext, - internalProfileId - ) + val intent = IntentFactoryShim.createRecentlyPlayedActivityIntent( + activity.applicationContext, + internalProfileId ) + activity.startActivity(intent) } } 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 6d202dc4a91..7978fec262c 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 @@ -6,7 +6,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import org.oppia.app.home.RouteToTopicPlayStoryListener import org.oppia.app.model.PromotedStory -import org.oppia.app.topic.TopicActivity +import org.oppia.app.shim.IntentFactoryShim import org.oppia.app.viewmodel.ObservableViewModel // TODO(#283): Add download status information to promoted-story-card. @@ -15,7 +15,8 @@ import org.oppia.app.viewmodel.ObservableViewModel class PromotedStoryViewModel( private val activity: AppCompatActivity, private val internalProfileId: Int, - val entityType: String + val entityType: String, + private val IntentFactoryShim: IntentFactoryShim ) : ObservableViewModel(), RouteToTopicPlayStoryListener { @@ -40,13 +41,12 @@ class PromotedStoryViewModel( } override fun routeToTopicPlayStory(internalProfileId: Int, topicId: String, storyId: String) { - activity.startActivity( - TopicActivity.createTopicPlayStoryActivityIntent( - activity.applicationContext, - internalProfileId, - topicId, - storyId - ) + val intent = IntentFactoryShim.createTopicPlayStoryActivityIntent( + activity.applicationContext, + internalProfileId, + topicId, + storyId ) + activity.startActivity(intent) } } 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 55f5338d3a7..10a6fbf8c07 100644 --- a/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt +++ b/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt @@ -3,7 +3,6 @@ package org.oppia.app.parser import android.content.Context import androidx.annotation.StringRes import org.oppia.app.R -import org.oppia.app.customview.interaction.FractionInputInteractionView import org.oppia.app.model.Fraction import org.oppia.domain.util.normalizeWhitespace diff --git a/app/src/main/java/org/oppia/app/player/state/DragDropSortInteractionView.kt b/app/src/main/java/org/oppia/app/player/state/DragDropSortInteractionView.kt index efbf937249a..8a0e726b4c3 100644 --- a/app/src/main/java/org/oppia/app/player/state/DragDropSortInteractionView.kt +++ b/app/src/main/java/org/oppia/app/player/state/DragDropSortInteractionView.kt @@ -5,18 +5,17 @@ import android.util.AttributeSet import android.view.LayoutInflater import androidx.core.view.isVisible import androidx.databinding.BindingAdapter -import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import org.oppia.app.databinding.DragDropInteractionItemsBinding -import org.oppia.app.databinding.DragDropSingleItemBinding -import org.oppia.app.fragment.InjectableFragment import org.oppia.app.player.state.itemviewmodel.DragDropInteractionContentViewModel import org.oppia.app.recyclerview.BindableAdapter import org.oppia.app.recyclerview.DragAndDropItemFacilitator import org.oppia.app.recyclerview.OnDragEndedListener import org.oppia.app.recyclerview.OnItemDragListener +import org.oppia.app.shim.ViewBindingShim +import org.oppia.app.shim.ViewComponentFactory import org.oppia.util.accessibility.CustomAccessibilityManager import org.oppia.util.gcsresource.DefaultResourceBucketName import org.oppia.util.parser.ExplorationHtmlParserEntityType @@ -50,11 +49,17 @@ class DragDropSortInteractionView @JvmOverloads constructor( @field:DefaultResourceBucketName lateinit var resourceBucketName: String + @Inject + lateinit var viewBindingShim: ViewBindingShim + private lateinit var entityId: String + private lateinit var onDragEnd: OnDragEndedListener + private lateinit var onItemDrag: OnItemDragListener override fun onAttachedToWindow() { super.onAttachedToWindow() - FragmentManager.findFragment(this).createViewComponent(this).inject(this) + (FragmentManager.findFragment(this) as ViewComponentFactory) + .createViewComponent(this).inject(this) isAccessibilityEnabled = accessibilityManager.isScreenReaderEnabled() } @@ -78,18 +83,24 @@ class DragDropSortInteractionView @JvmOverloads constructor( .newBuilder() .registerViewBinder( inflateView = { parent -> - DragDropInteractionItemsBinding.inflate( - LayoutInflater.from(parent.context), parent, /* attachToParent= */ false - ).root + viewBindingShim.provideDragDropSortInteractionInflatedView( + LayoutInflater.from(parent.context), + parent, + /* attachToParent= */ false + ) }, bindView = { view, viewModel -> - val binding = DataBindingUtil.findBinding(view)!! - binding.dragDropItemRecyclerview.adapter = createNestedAdapter() - binding.adapter = adapter - binding.dragDropContentGroupItem.isVisible = isMultipleItemsInSamePositionAllowed - binding.dragDropContentUnlinkItems.isVisible = viewModel.htmlContent.htmlList.size > 1 - binding.dragDropAccessibleContainer.isVisible = isAccessibilityEnabled - binding.viewModel = viewModel + viewBindingShim.setDragDropInteractionItemsBinding(view) + viewBindingShim.getDragDropInteractionItemsBindingRecyclerView().adapter = + createNestedAdapter() + adapter?.let { viewBindingShim.setDragDropInteractionItemsBindingAdapter(it) } + viewBindingShim.getDragDropInteractionItemsBindingGroupItem().isVisible = + isMultipleItemsInSamePositionAllowed + viewBindingShim.getDragDropInteractionItemsBindingUnlinkItems().isVisible = + viewModel.htmlContent.htmlList.size > 1 + viewBindingShim.getDragDropInteractionItemsBindingAccessibleContainer().isVisible = + isAccessibilityEnabled + viewBindingShim.setDragDropInteractionItemsBindingViewModel(viewModel) } ) .build() @@ -100,39 +111,49 @@ class DragDropSortInteractionView @JvmOverloads constructor( .newBuilder() .registerViewBinder( inflateView = { parent -> - DragDropSingleItemBinding.inflate( - LayoutInflater.from(parent.context), parent, /* attachToParent= */ false - ).root + viewBindingShim.provideDragDropSingleItemInflatedView( + LayoutInflater.from(parent.context), + parent, + /* attachToParent= */ false + ) }, bindView = { view, viewModel -> - val binding = DataBindingUtil.findBinding(view)!! - binding.htmlContent = htmlParserFactory.create( + viewBindingShim.setDragDropSingleItemBinding(view) + viewBindingShim.setDragDropSingleItemBindingHtmlContent( + htmlParserFactory, resourceBucketName, entityType, entityId, - /* imageCenterAlign= */ false + viewModel ) - .parseOppiaHtml( - viewModel, binding.dragDropContentTextView - ) } ) .build() } -} -/** Bind ItemTouchHelperSimpleCallback with RecyclerView for a [DragDropSortInteractionView] via data-binding. */ -@BindingAdapter(value = ["onDragEnded", "onItemDrag"], requireAll = false) -fun setItemDragToRecyclerView( - dragDropSortInteractionView: DragDropSortInteractionView, - onDragEnd: OnDragEndedListener, - onItemDrag: OnItemDragListener -) { - val dragCallback: ItemTouchHelper.Callback = - DragAndDropItemFacilitator(onItemDrag, onDragEnd) - - val itemTouchHelper = ItemTouchHelper(dragCallback) - itemTouchHelper.attachToRecyclerView(dragDropSortInteractionView) + fun setOnDragEnded(onDragEnd: OnDragEndedListener) { + this.onDragEnd = onDragEnd + checkIfSettingIsPossible() + } + + fun setOnItemDrag(onItemDrag: OnItemDragListener) { + this.onItemDrag = onItemDrag + checkIfSettingIsPossible() + } + + private fun checkIfSettingIsPossible() { + if (::onDragEnd.isInitialized && ::onItemDrag.isInitialized) { + performAttachment() + } + } + + private fun performAttachment() { + val dragCallback: ItemTouchHelper.Callback = + DragAndDropItemFacilitator(onItemDrag, onDragEnd) + + val itemTouchHelper = ItemTouchHelper(dragCallback) + itemTouchHelper.attachToRecyclerView(this) + } } /** Sets the exploration ID for a specific [DragDropSortInteractionView] via data-binding. */ diff --git a/app/src/main/java/org/oppia/app/player/state/ImageRegionSelectionInteractionView.kt b/app/src/main/java/org/oppia/app/player/state/ImageRegionSelectionInteractionView.kt index 4a2add5bd4c..426b1cc1bb2 100644 --- a/app/src/main/java/org/oppia/app/player/state/ImageRegionSelectionInteractionView.kt +++ b/app/src/main/java/org/oppia/app/player/state/ImageRegionSelectionInteractionView.kt @@ -6,10 +6,11 @@ import android.view.View import android.widget.FrameLayout import androidx.appcompat.widget.AppCompatImageView import androidx.core.view.forEachIndexed -import androidx.databinding.BindingAdapter +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import org.oppia.app.fragment.InjectableFragment import org.oppia.app.model.ImageWithRegions +import org.oppia.app.shim.ViewBindingShim +import org.oppia.app.shim.ViewComponentFactory import org.oppia.app.utility.ClickableAreasImage import org.oppia.app.utility.OnClickableAreaClickedListener import org.oppia.util.accessibility.CustomAccessibilityManager @@ -61,7 +62,12 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( @field:DefaultGcsPrefix lateinit var gcsPrefix: String + @Inject + lateinit var bindingInterface: ViewBindingShim + private lateinit var entityId: String + private lateinit var overlayView: FrameLayout + private lateinit var onRegionClicked: OnClickableAreaClickedListener /** * Sets the URL for the image & initiates loading it. This is intended to be called via data-binding. @@ -107,7 +113,8 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( val area = ClickableAreasImage( this, this.parent as FrameLayout, - listener + listener, + bindingInterface ) area.addRegionViews() } @@ -122,27 +129,40 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - FragmentManager.findFragment(this).createViewComponent(this).inject(this) + (FragmentManager.findFragment(this) as ViewComponentFactory) + .createViewComponent(this) + .inject(this) isAccessibilityEnabled = accessibilityManager.isScreenReaderEnabled() } -} -/** Bind ItemTouchHelperSimpleCallback with RecyclerView for a [DragDropSortInteractionView] via data-binding. */ -@BindingAdapter(value = ["onRegionClicked", "overlayView"], requireAll = false) -fun setRegionClickToImageView( - imageRegionSelectionInteractionView: ImageRegionSelectionInteractionView, - onClickableAreaClickedListener: OnClickableAreaClickedListener, - parentView: FrameLayout -) { - val area = ClickableAreasImage( - imageRegionSelectionInteractionView, - parentView, - onClickableAreaClickedListener - ) - - imageRegionSelectionInteractionView.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> // ktlint-disable max-line-length - // Update the regions, as the bounds have changed - if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) - area.addRegionViews() + fun setOnRegionClicked(onRegionClicked: OnClickableAreaClickedListener) { + this.onRegionClicked = onRegionClicked + checkIfSettingIsPossible() + } + + fun setOverlayView(overlayView: FrameLayout) { + this.overlayView = overlayView + checkIfSettingIsPossible() + } + + private fun checkIfSettingIsPossible() { + if (::onRegionClicked.isInitialized && ::overlayView.isInitialized) { + performAttachment() + } + } + + private fun performAttachment() { + val area = ClickableAreasImage( + this, + overlayView, + onRegionClicked, + bindingInterface + ) + + this.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> // ktlint-disable max-line-length + // Update the regions, as the bounds have changed + if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) + area.addRegionViews() + } } } 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 index ad3a2939673..42df791b83a 100644 --- a/app/src/main/java/org/oppia/app/player/state/SelectionInteractionView.kt +++ b/app/src/main/java/org/oppia/app/player/state/SelectionInteractionView.kt @@ -4,25 +4,19 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import androidx.databinding.BindingAdapter -import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment 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.player.state.itemviewmodel.SelectionItemInputType import org.oppia.app.recyclerview.BindableAdapter +import org.oppia.app.shim.ViewBindingShim +import org.oppia.app.shim.ViewComponentFactory import org.oppia.util.gcsresource.DefaultResourceBucketName 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. @@ -46,11 +40,15 @@ class SelectionInteractionView @JvmOverloads constructor( @field:DefaultResourceBucketName lateinit var resourceBucketName: String + @Inject + lateinit var bindingInterface: ViewBindingShim + private lateinit var entityId: String override fun onAttachedToWindow() { super.onAttachedToWindow() - FragmentManager.findFragment(this).createViewComponent(this).inject(this) + (FragmentManager.findFragment(this) as ViewComponentFactory) + .createViewComponent(this).inject(this) } fun setAllOptionsItemInputType(selectionItemInputType: SelectionItemInputType) { @@ -75,20 +73,21 @@ class SelectionInteractionView @JvmOverloads constructor( .newBuilder() .registerViewBinder( inflateView = { parent -> - ItemSelectionInteractionItemsBinding.inflate( - LayoutInflater.from(parent.context), parent, /* attachToParent= */ false - ).root + bindingInterface.provideSelectionInteractionViewInflatedView( + LayoutInflater.from(parent.context), + parent, + /* attachToParent= */ false + ) }, bindView = { view, viewModel -> - val binding = - DataBindingUtil.findBinding(view)!! - binding.htmlContent = - htmlParserFactory.create( - resourceBucketName, entityType, entityId, /* imageCenterAlign= */ false - ).parseOppiaHtml( - viewModel.htmlContent, binding.itemSelectionContentsTextView - ) - binding.viewModel = viewModel + bindingInterface.provideSelectionInteractionViewModel( + view, + viewModel, + htmlParserFactory, + resourceBucketName, + entityType, + entityId + ) } ) .build() @@ -97,20 +96,21 @@ class SelectionInteractionView @JvmOverloads constructor( .newBuilder() .registerViewBinder( inflateView = { parent -> - MultipleChoiceInteractionItemsBinding.inflate( - LayoutInflater.from(parent.context), parent, /* attachToParent= */ false - ).root + bindingInterface.provideMultipleChoiceInteractionItemsInflatedView( + LayoutInflater.from(parent.context), + parent, + /* attachToParent= */ false + ) }, bindView = { view, viewModel -> - val binding = - DataBindingUtil.findBinding(view)!! - binding.htmlContent = - htmlParserFactory.create( - resourceBucketName, entityType, entityId, /* imageCenterAlign= */ false - ).parseOppiaHtml( - viewModel.htmlContent, binding.multipleChoiceContentTextView - ) - binding.viewModel = viewModel + bindingInterface.provideMultipleChoiceInteractionItemsViewModel( + view, + viewModel, + htmlParserFactory, + resourceBucketName, + entityType, + entityId + ) } ) .build() @@ -118,12 +118,9 @@ class SelectionInteractionView @JvmOverloads constructor( } } -/** Sets the [SelectionItemInputType] for a specific [SelectionInteractionView] via data-binding. */ -@BindingAdapter("allOptionsItemInputType") -fun setAllOptionsItemInputType( - selectionInteractionView: SelectionInteractionView, - selectionItemInputType: SelectionItemInputType -) = selectionInteractionView.setAllOptionsItemInputType(selectionItemInputType) +fun setAllOptionsItemInputType(selectionItemInputType: SelectionItemInputType) { + setAllOptionsItemInputType(selectionItemInputType) +} /** Sets the exploration ID for a specific [SelectionInteractionView] via data-binding. */ @BindingAdapter("entityId") 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 917edd903f6..7007e00a512 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 @@ -96,7 +96,11 @@ class StateFragmentPresenter @Inject constructor( this.storyId = storyId this.explorationId = explorationId - binding = StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) + binding = StateFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) recyclerViewAssembler = createRecyclerViewAssembler( assemblerBuilderFactory.create(resourceBucketName, entityType), binding.congratulationsTextView @@ -120,7 +124,9 @@ class StateFragmentPresenter @Inject constructor( if (bottom < oldBottom) { binding.stateRecyclerView.postDelayed( { - binding.stateRecyclerView.scrollToPosition(stateRecyclerViewAdapter.itemCount - 1) + binding.stateRecyclerView.scrollToPosition( + stateRecyclerViewAdapter.itemCount - 1 + ) }, 100 ) @@ -173,7 +179,7 @@ class StateFragmentPresenter @Inject constructor( fun onSubmitButtonClicked() { hideKeyboard() - handleSubmitAnswer(viewModel.getPendingAnswer(recyclerViewAssembler)) + handleSubmitAnswer(viewModel.getPendingAnswer(recyclerViewAssembler::getPendingAnswerHandler)) } fun onResponsesHeaderClicked() { @@ -208,7 +214,7 @@ class StateFragmentPresenter @Inject constructor( fun handleKeyboardAction() { hideKeyboard() if (viewModel.getCanSubmitAnswer().get() == true) { - handleSubmitAnswer(viewModel.getPendingAnswer(recyclerViewAssembler)) + handleSubmitAnswer(viewModel.getPendingAnswer(recyclerViewAssembler::getPendingAnswerHandler)) } } @@ -281,7 +287,11 @@ class StateFragmentPresenter @Inject constructor( private fun processEphemeralStateResult(result: AsyncResult) { if (result.isFailure()) { - logger.e("StateFragment", "Failed to retrieve ephemeral state", result.getErrorOrNull()!!) + logger.e( + "StateFragment", + "Failed to retrieve ephemeral state", + result.getErrorOrNull()!! + ) return } else if (result.isPending()) { // Display nothing until a valid result is available. @@ -327,7 +337,8 @@ class StateFragmentPresenter @Inject constructor( /** * This function listens to the result of RevealHint. * Whenever a hint is revealed using ExplorationProgressController.submitHintIsRevealed function, - * this function will wait for the response from that function and based on which we can move to next state. + * this function will wait for the response from that function and based on which we can move to + * next state. */ private fun subscribeToHint(hintResultLiveData: LiveData>) { val hintLiveData = getHintIsRevealed(hintResultLiveData) @@ -345,7 +356,8 @@ class StateFragmentPresenter @Inject constructor( /** * This function listens to the result of RevealSolution. * Whenever a hint is revealed using ExplorationProgressController.submitHintIsRevealed function, - * this function will wait for the response from that function and based on which we can move to next state. + * this function will wait for the response from that function and based on which we can move to + * next state. */ private fun subscribeToSolution(solutionResultLiveData: LiveData>) { val solutionLiveData = getSolutionIsRevealed(solutionResultLiveData) @@ -363,7 +375,8 @@ class StateFragmentPresenter @Inject constructor( /** * This function listens to the result of submitAnswer. * Whenever an answer is submitted using ExplorationProgressController.submitAnswer function, - * this function will wait for the response from that function and based on which we can move to next state. + * this function will wait for the response from that function and based on which we can move to + * next state. */ private fun subscribeToAnswerOutcome( answerOutcomeResultLiveData: LiveData> 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 012ffc3ebf8..c7bacbf461a 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import org.oppia.app.fragment.FragmentScope import org.oppia.app.model.UserAnswer import org.oppia.app.player.state.answerhandling.AnswerErrorCategory +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.app.viewmodel.ObservableArrayList import org.oppia.app.viewmodel.ObservableViewModel @@ -48,25 +49,30 @@ class StateViewModel @Inject constructor() : ObservableViewModel() { fun getCanSubmitAnswer(): ObservableField = canSubmitAnswer fun getPendingAnswer( - statePlayerRecyclerViewAssembler: StatePlayerRecyclerViewAssembler + retrieveAnswerHandler: (List) -> InteractionAnswerHandler? ): UserAnswer { - return getPendingAnswerWithoutError(statePlayerRecyclerViewAssembler) - ?: UserAnswer.getDefaultInstance() + return getPendingAnswerWithoutError( + retrieveAnswerHandler( + getAnswerItemList() + ) + ) ?: UserAnswer.getDefaultInstance() } private fun getPendingAnswerWithoutError( - statePlayerRecyclerViewAssembler: StatePlayerRecyclerViewAssembler + answerHandler: InteractionAnswerHandler? ): UserAnswer? { - val items = if (isSplitView.get() == true) { - rightItemList - } else { - itemList - } - val answerHandler = statePlayerRecyclerViewAssembler.getPendingAnswerHandler(items) return if (answerHandler?.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME) == null) { answerHandler?.getPendingAnswer() } else { null } } + + private fun getAnswerItemList(): List { + return if (isSplitView.get() == true) { + rightItemList + } else { + itemList + } + } } 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 index 79fb2a402a0..9b4450ca04c 100644 --- 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 @@ -7,12 +7,17 @@ import org.oppia.app.model.Interaction import org.oppia.app.model.InteractionObject import org.oppia.app.model.StringList import org.oppia.app.model.UserAnswer -import org.oppia.app.player.state.SelectionItemInputType import org.oppia.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.app.viewmodel.ObservableArrayList +/** Corresponds to the type of input that should be used for an item selection interaction view. */ +enum class SelectionItemInputType { + CHECKBOXES, + RADIO_BUTTONS +} + /** [StateItemViewModel] for multiple or item-selection input choice list. */ class SelectionInteractionViewModel( val entityId: String, diff --git a/app/src/main/java/org/oppia/app/profile/ProfileInputView.kt b/app/src/main/java/org/oppia/app/profile/ProfileInputView.kt index c19f595830b..492bedfddf3 100644 --- a/app/src/main/java/org/oppia/app/profile/ProfileInputView.kt +++ b/app/src/main/java/org/oppia/app/profile/ProfileInputView.kt @@ -10,9 +10,12 @@ import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import androidx.databinding.BindingAdapter -import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import org.oppia.app.R -import org.oppia.app.databinding.ProfileInputViewBinding +import org.oppia.app.shim.ViewBindingShim +import org.oppia.app.shim.ViewComponentFactory +import javax.inject.Inject /** Custom view that is used for name or pin input with error messages. */ class ProfileInputView @JvmOverloads constructor( @@ -20,6 +23,16 @@ class ProfileInputView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { + + @Inject + lateinit var bindingInterface: ViewBindingShim + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + (FragmentManager.findFragment(this) as ViewComponentFactory) + .createViewComponent(this).inject(this) + } + companion object { @JvmStatic @BindingAdapter("profile:label") @@ -27,42 +40,11 @@ class ProfileInputView @JvmOverloads constructor( profileInputView.label.text = label } - @JvmStatic - @BindingAdapter("profile:labelMargin") - fun setLayoutMarginStart(profileInputView: ProfileInputView, dimen: Float) { - val layoutParams = profileInputView.label.layoutParams as MarginLayoutParams - layoutParams.marginStart = dimen.toInt() - profileInputView.label.layoutParams = layoutParams - } - - @JvmStatic - @BindingAdapter("profile:inputLength") - fun setInputLength(profileInputView: ProfileInputView, inputLength: Int) { - profileInputView.input.filters = arrayOf(InputFilter.LengthFilter(inputLength)) - } - - @JvmStatic - @BindingAdapter("profile:error") - fun setProfileImage(profileInputView: ProfileInputView, errorMessage: String?) { - var errMessage: String = errorMessage ?: "" - if (errMessage.isEmpty()) { - profileInputView.clearErrorText() - } else { - profileInputView.setErrorText(errMessage) - } - } - /** Binding adapter for setting a [TextWatcher] as a change listener for an [EditText]. */ @BindingAdapter("android:addTextChangedListener") fun bindTextWatcher(editText: EditText, textWatcher: TextWatcher) { editText.addTextChangedListener(textWatcher) } - - @JvmStatic - @BindingAdapter("profile:singleLine") - fun setSingleLine(profileInputView: ProfileInputView, type: Boolean) { - profileInputView.input.setSingleLine(type) - } } private var label: TextView @@ -70,16 +52,23 @@ class ProfileInputView @JvmOverloads constructor( private var input: EditText init { - val binding = DataBindingUtil.inflate( + val attributes = context.obtainStyledAttributes(attrs, R.styleable.ProfileInputView) + label = bindingInterface.provideProfileInputViewBindingLabelText( LayoutInflater.from(context), - R.layout.profile_input_view, this, - /* attachToRoot= */ true + this, + true + ) + label.text = attributes.getString(R.styleable.ProfileInputView_label) + input = bindingInterface.provideProfileInputViewBindingInput( + LayoutInflater.from(context), + this, + true + ) + errorText = bindingInterface.provideProfileInputViewBindingErrorText( + LayoutInflater.from(context), + this, + true ) - val attributes = context.obtainStyledAttributes(attrs, R.styleable.ProfileInputView) - binding.labelText.text = attributes.getString(R.styleable.ProfileInputView_label) - label = binding.labelText - input = binding.input - errorText = binding.errorText orientation = VERTICAL if ( attributes.getBoolean( @@ -123,6 +112,29 @@ class ProfileInputView @JvmOverloads constructor( errorText.text = errorMessage } + fun setSingleLine(type: Boolean) { + input.setSingleLine(type) + } + + fun setInputLength(inputLength: Int) { + input.filters = arrayOf(InputFilter.LengthFilter(inputLength)) + } + + fun setLabelMargin(dimen: Float) { + val layoutParams = label.layoutParams as MarginLayoutParams + layoutParams.marginStart = dimen.toInt() + label.layoutParams = layoutParams + } + + fun setError(errorMessage: String) { + val errMessage: String = errorMessage ?: "" + if (errMessage.isEmpty()) { + clearErrorText() + } else { + setErrorText(errMessage) + } + } + fun setLabel(labelText: String) { label.text = labelText } diff --git a/app/src/main/java/org/oppia/app/settings/profile/ProfileEditActivityPresenter.kt b/app/src/main/java/org/oppia/app/settings/profile/ProfileEditActivityPresenter.kt index 39554484286..9cd0b1dd857 100644 --- a/app/src/main/java/org/oppia/app/settings/profile/ProfileEditActivityPresenter.kt +++ b/app/src/main/java/org/oppia/app/settings/profile/ProfileEditActivityPresenter.kt @@ -1,6 +1,7 @@ package org.oppia.app.settings.profile import android.content.Intent +import android.widget.Switch import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil @@ -35,7 +36,10 @@ class ProfileEditActivityPresenter @Inject constructor( R.layout.profile_edit_activity ) val profileId = activity.intent.getIntExtra(KEY_PROFILE_EDIT_PROFILE_ID, 0) - editViewModel.setProfileId(profileId) + editViewModel.setProfileId( + profileId, + activity.findViewById(R.id.profile_edit_allow_download_switch) + ) binding.apply { viewModel = editViewModel lifecycleOwner = activity diff --git a/app/src/main/java/org/oppia/app/settings/profile/ProfileEditViewModel.kt b/app/src/main/java/org/oppia/app/settings/profile/ProfileEditViewModel.kt index 5949d131932..1a59305c91c 100644 --- a/app/src/main/java/org/oppia/app/settings/profile/ProfileEditViewModel.kt +++ b/app/src/main/java/org/oppia/app/settings/profile/ProfileEditViewModel.kt @@ -4,7 +4,6 @@ import android.widget.Switch import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations -import org.oppia.app.R import org.oppia.app.activity.ActivityScope import org.oppia.app.model.Profile import org.oppia.app.model.ProfileId @@ -14,6 +13,7 @@ import org.oppia.util.data.AsyncResult import org.oppia.util.logging.ConsoleLogger import javax.inject.Inject +// TODO(#1633): Fix ViewModel to not depend on View /** The ViewModel for [ProfileEditActivity]. */ @ActivityScope class ProfileEditViewModel @Inject constructor( @@ -22,6 +22,7 @@ class ProfileEditViewModel @Inject constructor( private val profileManagementController: ProfileManagementController ) : ObservableViewModel() { private lateinit var profileId: ProfileId + private lateinit var switch: Switch lateinit var profileName: String @@ -34,8 +35,9 @@ class ProfileEditViewModel @Inject constructor( var isAdmin = false - fun setProfileId(id: Int) { + fun setProfileId(id: Int, switch: Switch) { profileId = ProfileId.newBuilder().setInternalId(id).build() + this.switch = switch } private fun processGetProfileResult(profileResult: AsyncResult): Profile { @@ -47,7 +49,6 @@ class ProfileEditViewModel @Inject constructor( ) } val profile = profileResult.getOrDefault(Profile.getDefaultInstance()) - val switch = activity.findViewById(R.id.profile_edit_allow_download_switch) switch.isChecked = profile.allowDownloadAccess activity.title = profile.name profileName = profile.name diff --git a/app/src/main/java/org/oppia/app/shim/IntentFactoryShim.kt b/app/src/main/java/org/oppia/app/shim/IntentFactoryShim.kt new file mode 100644 index 00000000000..4676edc3411 --- /dev/null +++ b/app/src/main/java/org/oppia/app/shim/IntentFactoryShim.kt @@ -0,0 +1,37 @@ +package org.oppia.app.shim + +import android.content.Context +import android.content.Intent +import androidx.fragment.app.FragmentActivity + +/** + * Creates intents for ViewModels in order to avoid ViewModel files directly depending on Activites. + * When working on a ViewModel file, developers should refrain from directly referencing Activities + * by adding all Intent functionality here. + * + * Please note that this file is temporary and all functionality will be returned to its respective + * ViewModel once Gradle has been removed. + */ +// TODO(#1619): Remove file post-Gradle +interface IntentFactoryShim { + + /** Returns [ProfileActivity] intent for [AdministratorControlsAccountActionsViewModel]. */ + fun createProfileActivityIntent(fragment: FragmentActivity): Intent + + /** + * Creates a [TopicActivity] intent for [PromotedStoryViewModel] and passes necessary string + * data. + * */ + fun createTopicPlayStoryActivityIntent( + context: Context, + internalProfileId: Int, + topicId: String, + storyId: String + ): Intent + + /** + * Creates a [RecentlyPlayedActivity] intent for [PromotedStoryListViewModel] and passes + * necessary string data. + * */ + fun createRecentlyPlayedActivityIntent(context: Context, internalProfileId: Int): Intent +} diff --git a/app/src/main/java/org/oppia/app/shim/IntentFactoryShimImpl.kt b/app/src/main/java/org/oppia/app/shim/IntentFactoryShimImpl.kt new file mode 100644 index 00000000000..4d57271bff5 --- /dev/null +++ b/app/src/main/java/org/oppia/app/shim/IntentFactoryShimImpl.kt @@ -0,0 +1,63 @@ +package org.oppia.app.shim + +import android.content.Context +import android.content.Intent +import androidx.fragment.app.FragmentActivity +import org.oppia.app.drawer.KEY_NAVIGATION_PROFILE_ID +import org.oppia.app.home.recentlyplayed.RecentlyPlayedActivity +import org.oppia.app.profile.ProfileActivity +import org.oppia.app.topic.TopicActivity +import javax.inject.Inject + +/** + * Creates intents for ViewModels in order to avoid ViewModel files directly depending on Activites. + * When working on a ViewModel file, developers should refrain from directly referencing Activities + * by adding all Intent functionality here. + * + * Please note that this file is temporary and all functionality will be returned to its respective + * ViewModel once Gradle has been removed. + */ +// TODO(#1619): Remove file post-Gradle +class IntentFactoryShimImpl @Inject constructor() : IntentFactoryShim { + + private val TOPIC_ACTIVITY_TOPIC_ID_ARGUMENT_KEY = "TopicActivity.topic_id" + private val TOPIC_ACTIVITY_STORY_ID_ARGUMENT_KEY = "TopicActivity.story_id" + + /** Returns [ProfileActivity] intent for [AdministratorControlsAccountActionsViewModel]. */ + override fun createProfileActivityIntent(fragment: FragmentActivity): Intent { + return Intent(fragment, ProfileActivity::class.java) + } + + /** + * Creates a [TopicActivity] intent for [PromotedStoryViewModel] and passes necessary string + * data. + * */ + override fun createTopicPlayStoryActivityIntent( + context: Context, + internalProfileId: Int, + topicId: String, + storyId: String + ): Intent { + val intent = Intent(context, TopicActivity::class.java) + intent.putExtra(KEY_NAVIGATION_PROFILE_ID, internalProfileId) + intent.putExtra(TOPIC_ACTIVITY_TOPIC_ID_ARGUMENT_KEY, topicId) + intent.putExtra(TOPIC_ACTIVITY_STORY_ID_ARGUMENT_KEY, storyId) + return intent + } + + /** + * Creates a [RecentlyPlayedActivity] intent for [PromotedStoryListViewModel] and passes + * necessary string data. + * */ + override fun createRecentlyPlayedActivityIntent( + context: Context, + internalProfileId: Int + ): Intent { + val intent = Intent(context, RecentlyPlayedActivity::class.java) + intent.putExtra( + RecentlyPlayedActivity.RECENTLY_PLAYED_ACTIVITY_INTERNAL_PROFILE_ID_KEY, + internalProfileId + ) + return intent + } +} diff --git a/app/src/main/java/org/oppia/app/shim/IntentFactoryShimModule.kt b/app/src/main/java/org/oppia/app/shim/IntentFactoryShimModule.kt new file mode 100644 index 00000000000..3021aa97811 --- /dev/null +++ b/app/src/main/java/org/oppia/app/shim/IntentFactoryShimModule.kt @@ -0,0 +1,11 @@ +package org.oppia.app.shim + +import dagger.Binds +import dagger.Module + +@Module +interface IntentFactoryShimModule { + + @Binds + fun provideIntentFactoryShim(intentFactoryShim: IntentFactoryShimImpl): IntentFactoryShim +} diff --git a/app/src/main/java/org/oppia/app/shim/ViewBindingShim.kt b/app/src/main/java/org/oppia/app/shim/ViewBindingShim.kt new file mode 100644 index 00000000000..fa3962a5b77 --- /dev/null +++ b/app/src/main/java/org/oppia/app/shim/ViewBindingShim.kt @@ -0,0 +1,163 @@ +package org.oppia.app.shim + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import org.oppia.app.player.state.itemviewmodel.DragDropInteractionContentViewModel +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionContentViewModel +import org.oppia.util.parser.HtmlParser + +/** + * Creates bindings for Views in order to avoid View files directly depending on Binding files. + * When working on a View file, developers should refrain from directly referencing Binding files + * by adding all related functionality here. + * + * Please note that this file is temporary and all functionality will be returned to it's respective + * View once Gradle has been removed. + */ +// TODO(#1619): Remove file post-Gradle +interface ViewBindingShim { + + /** + * Handles binding inflation for [ProfileInputView] and returns the binding's label text. + */ + fun provideProfileInputViewBindingLabelText( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): TextView + + /** + * Handles binding inflation for [ProfileInputView] and returns the binding's input. + */ + fun provideProfileInputViewBindingInput( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): EditText + + /** + * Handles binding inflation for [ProfileInputView] and returns the binding's error text. + */ + fun provideProfileInputViewBindingErrorText( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): TextView + + /** + * Handles binding inflation for [SelectionInteractionView]'s ItemSelectionInteraction and + * returns the binding's root. + */ + fun provideSelectionInteractionViewInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View + + /** + * Handles binding inflation for [SelectionInteractionView]'s ItemSelectionInteraction and + * returns the binding's view model. + */ + fun provideSelectionInteractionViewModel( + view: View, + viewModel: SelectionInteractionContentViewModel, + htmlParserFactory: HtmlParser.Factory, + resourceBucketName: String, + entityType: String, + entityId: String + ) + + /** + * Handles binding inflation for [SelectionInteractionView]'s MultipleChoiceInteraction and + * returns the binding's view. + */ + fun provideMultipleChoiceInteractionItemsInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View + + /** + * Handles binding inflation for [SelectionInteractionView]'s MultipleChoiceInteraction and + * returns the binding's view model. + */ + fun provideMultipleChoiceInteractionItemsViewModel( + view: View, + viewModel: SelectionInteractionContentViewModel, + htmlParserFactory: HtmlParser.Factory, + resourceBucketName: String, + entityType: String, + entityId: String + ) + + /** + * Handles binding inflation for [DragDropSortInteractionView]'s SortInteraction and returns the + * binding's view. + */ + fun provideDragDropSortInteractionInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View + + /** Handles setting [DragDropInteractionItemsBinding]. */ + fun setDragDropInteractionItemsBinding( + view: View + ) + + /** Handles setting [DragDropInteractionItemsBinding]'s adapter. */ + fun setDragDropInteractionItemsBindingAdapter( + adapter: RecyclerView.Adapter + ) + + /** Returns [DragDropInteractionItemsBinding]'s RecyclerView. */ + fun getDragDropInteractionItemsBindingRecyclerView(): RecyclerView + + /** Returns [DragDropInteractionItemsBinding]'s dragDropContentGroupItem. */ + fun getDragDropInteractionItemsBindingGroupItem(): ImageButton + + /** Returns [DragDropInteractionItemsBinding]'s dragDropContentUnlinkItems. */ + fun getDragDropInteractionItemsBindingUnlinkItems(): ImageButton + + /** Returns [DragDropInteractionItemsBinding]'s dragDropAccessibleContainer. */ + fun getDragDropInteractionItemsBindingAccessibleContainer(): LinearLayout + + /** Handles setting [DragDropInteractionItemsBinding]'s view model. */ + fun setDragDropInteractionItemsBindingViewModel( + viewModel: DragDropInteractionContentViewModel + ) + + /** + * Handles binding inflation for [DragDropSortInteractionView]'s SingleItemInteraction and returns + * the binding's view. + */ + fun provideDragDropSingleItemInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View + + /** Handles setting [DragDropSingleItemBinding]. */ + fun setDragDropSingleItemBinding( + view: View + ) + + /** Handles setting [DragDropSingleItemBinding]'s html content. */ + fun setDragDropSingleItemBindingHtmlContent( + htmlParserFactory: HtmlParser.Factory, + resourceBucketName: String, + entityType: String, + entityId: String, + viewModel: String + ) + + /** Returns [ClickableAreasImage]'s default region. */ + fun getDefaultRegion(parentView: FrameLayout): View +} diff --git a/app/src/main/java/org/oppia/app/shim/ViewBindingShimImpl.kt b/app/src/main/java/org/oppia/app/shim/ViewBindingShimImpl.kt new file mode 100644 index 00000000000..19994a8e845 --- /dev/null +++ b/app/src/main/java/org/oppia/app/shim/ViewBindingShimImpl.kt @@ -0,0 +1,229 @@ +package org.oppia.app.shim + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.TextView +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.RecyclerView +import org.oppia.app.R +import org.oppia.app.databinding.DragDropInteractionItemsBinding +import org.oppia.app.databinding.DragDropSingleItemBinding +import org.oppia.app.databinding.ItemSelectionInteractionItemsBinding +import org.oppia.app.databinding.MultipleChoiceInteractionItemsBinding +import org.oppia.app.databinding.ProfileInputViewBinding +import org.oppia.app.player.state.itemviewmodel.DragDropInteractionContentViewModel +import org.oppia.app.player.state.itemviewmodel.SelectionInteractionContentViewModel +import org.oppia.util.parser.HtmlParser +import javax.inject.Inject + +/** + * Creates bindings for Views in order to avoid View files directly depending on Binding files. + * When working on a View file, developers should refrain from directly referencing Binding files + * by adding all related functionality here. + * + * Please note that this file is temporary and all functionality will be returned to it's respective + * View once Gradle has been removed. + */ +// TODO(#1619): Remove file post-Gradle +class ViewBindingShimImpl @Inject constructor() : ViewBindingShim { + + override fun provideProfileInputViewBindingLabelText( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): TextView { + val binding = DataBindingUtil.inflate( + inflater, + R.layout.profile_input_view, + parent, + attachToParent + ) + return binding.labelText + } + + override fun provideProfileInputViewBindingInput( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): EditText { + val binding = DataBindingUtil.inflate( + inflater, + R.layout.profile_input_view, + parent, + attachToParent + ) + return binding.input + } + + override fun provideProfileInputViewBindingErrorText( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): TextView { + val binding = DataBindingUtil.inflate( + inflater, + R.layout.profile_input_view, + parent, + attachToParent + ) + return binding.errorText + } + + override fun provideSelectionInteractionViewInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View { + return ItemSelectionInteractionItemsBinding.inflate( + LayoutInflater.from(parent.context), + parent, + /* attachToParent= */ false + ).root + } + + override fun provideSelectionInteractionViewModel( + view: View, + viewModel: SelectionInteractionContentViewModel, + htmlParserFactory: HtmlParser.Factory, + resourceBucketName: String, + entityType: String, + entityId: String + ) { + val binding = + DataBindingUtil.findBinding(view)!! + binding.htmlContent = + htmlParserFactory.create( + resourceBucketName, + entityType, + entityId, + false + ).parseOppiaHtml( + viewModel.htmlContent, + binding.itemSelectionContentsTextView + ) + binding.viewModel = viewModel + } + + override fun provideMultipleChoiceInteractionItemsInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View { + return MultipleChoiceInteractionItemsBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ).root + } + + override fun provideMultipleChoiceInteractionItemsViewModel( + view: View, + viewModel: SelectionInteractionContentViewModel, + htmlParserFactory: HtmlParser.Factory, + resourceBucketName: String, + entityType: String, + entityId: String + ) { + val binding = + DataBindingUtil.findBinding(view)!! + binding.htmlContent = + htmlParserFactory.create( + resourceBucketName, entityType, entityId, /* imageCenterAlign= */ false + ).parseOppiaHtml( + viewModel.htmlContent, binding.multipleChoiceContentTextView + ) + binding.viewModel = viewModel + } + + override fun provideDragDropSortInteractionInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View { + return DragDropInteractionItemsBinding.inflate( + LayoutInflater.from(parent.context), parent, /* attachToParent= */ false + ).root + } + + private lateinit var dragDropInteractionItemsBinding: DragDropInteractionItemsBinding + + override fun setDragDropInteractionItemsBinding( + view: View + ) { + dragDropInteractionItemsBinding = + DataBindingUtil.findBinding(view)!! + } + + override fun setDragDropInteractionItemsBindingAdapter( + adapter: RecyclerView.Adapter + ) { + dragDropInteractionItemsBinding.adapter = adapter + } + + override fun getDragDropInteractionItemsBindingRecyclerView(): RecyclerView { + return dragDropInteractionItemsBinding.dragDropItemRecyclerview + } + + override fun getDragDropInteractionItemsBindingGroupItem(): ImageButton { + return dragDropInteractionItemsBinding.dragDropContentGroupItem + } + + override fun getDragDropInteractionItemsBindingUnlinkItems(): ImageButton { + return dragDropInteractionItemsBinding.dragDropContentUnlinkItems + } + + override fun getDragDropInteractionItemsBindingAccessibleContainer(): LinearLayout { + return dragDropInteractionItemsBinding.dragDropAccessibleContainer + } + + override fun setDragDropInteractionItemsBindingViewModel( + viewModel: DragDropInteractionContentViewModel + ) { + dragDropInteractionItemsBinding.viewModel = viewModel + } + + override fun provideDragDropSingleItemInflatedView( + inflater: LayoutInflater, + parent: ViewGroup, + attachToParent: Boolean + ): View { + return DragDropSingleItemBinding.inflate( + LayoutInflater.from(parent.context), parent, /* attachToParent= */ false + ).root + } + + private lateinit var dragDropSingleItemBinding: DragDropSingleItemBinding + + override fun setDragDropSingleItemBinding( + view: View + ) { + dragDropSingleItemBinding = + DataBindingUtil.findBinding(view)!! + } + + override fun setDragDropSingleItemBindingHtmlContent( + htmlParserFactory: HtmlParser.Factory, + resourceBucketName: String, + entityType: String, + entityId: String, + viewModel: String + ) { + dragDropSingleItemBinding.htmlContent = htmlParserFactory.create( + resourceBucketName, + entityType, + entityId, + /* imageCenterAlign= */ false + ).parseOppiaHtml( + viewModel, dragDropSingleItemBinding.dragDropContentTextView + ) + } + + override fun getDefaultRegion(parentView: FrameLayout): View { + return parentView.findViewById(R.id.default_selected_region) + } +} diff --git a/app/src/main/java/org/oppia/app/shim/ViewBindingShimModule.kt b/app/src/main/java/org/oppia/app/shim/ViewBindingShimModule.kt new file mode 100644 index 00000000000..870b9789614 --- /dev/null +++ b/app/src/main/java/org/oppia/app/shim/ViewBindingShimModule.kt @@ -0,0 +1,11 @@ +package org.oppia.app.shim + +import dagger.Binds +import dagger.Module + +@Module +interface ViewBindingShimModule { + + @Binds + fun provideViewBindingShim(viewBindingShim: ViewBindingShimImpl): ViewBindingShim +} diff --git a/app/src/main/java/org/oppia/app/shim/ViewComponentFactory.kt b/app/src/main/java/org/oppia/app/shim/ViewComponentFactory.kt new file mode 100644 index 00000000000..e8690bcd687 --- /dev/null +++ b/app/src/main/java/org/oppia/app/shim/ViewComponentFactory.kt @@ -0,0 +1,11 @@ +package org.oppia.app.shim + +import android.view.View +import org.oppia.app.view.ViewComponent + +interface ViewComponentFactory { + /** + * Returns a new [ViewComponent] for the specified view. + */ + fun createViewComponent(view: View): ViewComponent +} 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 0d1b878a918..f49ab4dd98e 100644 --- a/app/src/main/java/org/oppia/app/story/StoryFragment.kt +++ b/app/src/main/java/org/oppia/app/story/StoryFragment.kt @@ -13,7 +13,7 @@ private const val KEY_TOPIC_ID_ARGUMENT = "TOPIC_ID" private const val KEY_STORY_ID_ARGUMENT = "STORY_ID" /** Fragment for displaying a story. */ -class StoryFragment : InjectableFragment(), ExplorationSelectionListener { +class StoryFragment : InjectableFragment(), ExplorationSelectionListener, StoryFragmentScroller { companion object { /** Returns a new [StoryFragment] to display the story corresponding to the specified story ID. */ fun newInstance(internalProfileId: Int, topicId: String, storyId: String): StoryFragment { @@ -77,7 +77,7 @@ class StoryFragment : InjectableFragment(), ExplorationSelectionListener { ) } - fun smoothScrollToPosition(position: Int) { + override fun smoothScrollToPosition(position: Int) { storyFragmentPresenter.smoothScrollToPosition(position) } } diff --git a/app/src/main/java/org/oppia/app/story/StoryFragmentScroller.kt b/app/src/main/java/org/oppia/app/story/StoryFragmentScroller.kt new file mode 100644 index 00000000000..84f438dc3cb --- /dev/null +++ b/app/src/main/java/org/oppia/app/story/StoryFragmentScroller.kt @@ -0,0 +1,9 @@ +package org.oppia.app.story + +interface StoryFragmentScroller { + /** + * Scrolls smoothly (with animation) to the specified vertical pixel position in + * [StoryFragment]. + * */ + fun smoothScrollToPosition(position: Int) +} diff --git a/app/src/main/java/org/oppia/app/story/StoryViewModel.kt b/app/src/main/java/org/oppia/app/story/StoryViewModel.kt index a2db4863150..200e96d8eeb 100644 --- a/app/src/main/java/org/oppia/app/story/StoryViewModel.kt +++ b/app/src/main/java/org/oppia/app/story/StoryViewModel.kt @@ -19,7 +19,7 @@ import org.oppia.util.logging.ConsoleLogger import org.oppia.util.parser.StoryHtmlParserEntityType import javax.inject.Inject -/** The ViewModel for [StoryFragment]. */ +/** The ViewModel for StoryFragment. */ @FragmentScope class StoryViewModel @Inject constructor( private val fragment: Fragment, @@ -83,7 +83,7 @@ class StoryViewModel @Inject constructor( val chapterList: List = storySummary.chapterList for (position in chapterList.indices) { if (storySummary.chapterList[position].chapterPlayState == ChapterPlayState.NOT_STARTED) { - (fragment as StoryFragment).smoothScrollToPosition(position + 1) + (fragment as StoryFragmentScroller).smoothScrollToPosition(position + 1) break } } diff --git a/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt b/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt index bd2098ec8ab..46427df2a66 100644 --- a/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.Observer import org.oppia.app.model.ChapterSummary import org.oppia.app.model.LessonThumbnail import org.oppia.app.story.ExplorationSelectionListener -import org.oppia.app.story.StoryFragment import org.oppia.domain.exploration.ExplorationDataController import org.oppia.util.data.AsyncResult import org.oppia.util.logging.ConsoleLogger diff --git a/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryHeaderViewModel.kt b/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryHeaderViewModel.kt index f2caa9b114b..a0430576b2f 100644 --- a/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryHeaderViewModel.kt +++ b/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryHeaderViewModel.kt @@ -1,7 +1,5 @@ package org.oppia.app.story.storyitemviewmodel -import org.oppia.app.story.StoryFragment - /** Header view model for the recycler view in [StoryFragment]. */ class StoryHeaderViewModel( val completedChapters: Int, diff --git a/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryItemViewModel.kt b/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryItemViewModel.kt index 2aadbd85d32..f4d4a51ee54 100644 --- a/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryItemViewModel.kt +++ b/app/src/main/java/org/oppia/app/story/storyitemviewmodel/StoryItemViewModel.kt @@ -1,6 +1,5 @@ package org.oppia.app.story.storyitemviewmodel -import org.oppia.app.story.StoryFragment import org.oppia.app.viewmodel.ObservableViewModel /** Super-class for generalising different views for the recyclerView in [StoryFragment] */ diff --git a/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragmentPresenter.kt b/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragmentPresenter.kt index dfe6da2bb42..1078ae4763a 100644 --- a/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardFragmentPresenter.kt @@ -33,10 +33,11 @@ class ConceptCardFragmentPresenter @Inject constructor( container, /* attachToRoot= */ false ) + val view = binding.conceptCardExplanationText val viewModel = getConceptCardViewModel() skillId = id - viewModel.setSkillIdAndBinding(skillId, binding) + viewModel.setSkillIdAndBinding(skillId, view) logConceptCardEvent(skillId) binding.conceptCardToolbar.setNavigationIcon(R.drawable.ic_close_white_24dp) diff --git a/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardViewModel.kt b/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardViewModel.kt index 401164989cb..6b8445ffedc 100644 --- a/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/conceptcard/ConceptCardViewModel.kt @@ -1,9 +1,9 @@ package org.oppia.app.topic.conceptcard +import android.widget.TextView import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel -import org.oppia.app.databinding.ConceptCardFragmentBinding import org.oppia.app.fragment.FragmentScope import org.oppia.app.model.ConceptCard import org.oppia.domain.topic.TopicController @@ -14,6 +14,7 @@ import org.oppia.util.parser.ConceptCardHtmlParserEntityType import org.oppia.util.parser.HtmlParser import javax.inject.Inject +// TODO(#1633): Fix ViewModel to not depend on View /** [ViewModel] for concept card, providing rich text and worked examples */ @FragmentScope class ConceptCardViewModel @Inject constructor( @@ -24,7 +25,7 @@ class ConceptCardViewModel @Inject constructor( @DefaultResourceBucketName private val resourceBucketName: String ) : ViewModel() { private lateinit var skillId: String - private lateinit var binding: ConceptCardFragmentBinding + private lateinit var view: TextView val conceptCardLiveData: LiveData by lazy { processConceptCardLiveData() @@ -35,9 +36,9 @@ class ConceptCardViewModel @Inject constructor( } /** Sets the value of skillId and binding. Must be called before setting ViewModel to binding */ - fun setSkillIdAndBinding(id: String, binding: ConceptCardFragmentBinding) { + fun setSkillIdAndBinding(id: String, view: TextView) { skillId = id - this.binding = binding + this.view = view } private val conceptCardResultLiveData: LiveData> by lazy { @@ -74,6 +75,6 @@ class ConceptCardViewModel @Inject constructor( val conceptCard = conceptCardResult.getOrDefault(ConceptCard.getDefaultInstance()) return htmlParserFactory .create(resourceBucketName, entityType, skillId, /* imageCenterAlign= */true) - .parseOppiaHtml(conceptCard.explanation.html, binding.conceptCardExplanationText) + .parseOppiaHtml(conceptCard.explanation.html, view) } } diff --git a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeFooterViewModel.kt b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeFooterViewModel.kt index db366b85c85..9eefef94158 100644 --- a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeFooterViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeFooterViewModel.kt @@ -1,6 +1,4 @@ package org.oppia.app.topic.practice.practiceitemviewmodel -import org.oppia.app.topic.practice.TopicPracticeFragment - /** Footer view model for the recycler view in [TopicPracticeFragment]. */ class TopicPracticeFooterViewModel : TopicPracticeItemViewModel() diff --git a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeHeaderViewModel.kt b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeHeaderViewModel.kt index 63deda3477e..32b30195836 100644 --- a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeHeaderViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeHeaderViewModel.kt @@ -1,6 +1,4 @@ package org.oppia.app.topic.practice.practiceitemviewmodel -import org.oppia.app.topic.practice.TopicPracticeFragment - /** Header view model for the recycler view in [TopicPracticeFragment]. */ class TopicPracticeHeaderViewModel : TopicPracticeItemViewModel() diff --git a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeItemViewModel.kt b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeItemViewModel.kt index b68f741264b..9d17c4bc894 100644 --- a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeItemViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeItemViewModel.kt @@ -1,6 +1,5 @@ package org.oppia.app.topic.practice.practiceitemviewmodel -import org.oppia.app.topic.practice.TopicPracticeFragment import org.oppia.app.viewmodel.ObservableViewModel /** Super-class for generalising different views for the recyclerView in [TopicPracticeFragment] */ diff --git a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt index a2eca76bfba..986d36f7fb2 100644 --- a/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/practiceitemviewmodel/TopicPracticeSubtopicViewModel.kt @@ -1,7 +1,6 @@ package org.oppia.app.topic.practice.practiceitemviewmodel import org.oppia.app.model.Subtopic -import org.oppia.app.topic.practice.TopicPracticeFragment /** Subtopic view model for the recycler view in [TopicPracticeFragment]. */ class TopicPracticeSubtopicViewModel(val subtopic: Subtopic) : TopicPracticeItemViewModel() diff --git a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt index a8810b7c77d..abf9e8b8478 100644 --- a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt @@ -164,7 +164,11 @@ class QuestionPlayerFragmentPresenter @Inject constructor( fun onSubmitButtonClicked() { hideKeyboard() - handleSubmitAnswer(questionViewModel.getPendingAnswer(recyclerViewAssembler)) + handleSubmitAnswer( + questionViewModel.getPendingAnswer( + recyclerViewAssembler::getPendingAnswerHandler + ) + ) } fun onResponsesHeaderClicked() { diff --git a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerViewModel.kt b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerViewModel.kt index 7ba089dae5b..fc0607bc5cc 100644 --- a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerViewModel.kt @@ -5,8 +5,8 @@ import androidx.databinding.ObservableBoolean import androidx.databinding.ObservableField import androidx.databinding.ObservableList import org.oppia.app.model.UserAnswer -import org.oppia.app.player.state.StatePlayerRecyclerViewAssembler import org.oppia.app.player.state.answerhandling.AnswerErrorCategory +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 @@ -43,25 +43,30 @@ class QuestionPlayerViewModel @Inject constructor() : ObservableViewModel() { fun getCanSubmitAnswer(): ObservableField = canSubmitAnswer fun getPendingAnswer( - recyclerViewAssembler: StatePlayerRecyclerViewAssembler + retrieveAnswerHandler: (List) -> InteractionAnswerHandler? ): UserAnswer { - return getPendingAnswerWithoutError(recyclerViewAssembler) ?: UserAnswer.getDefaultInstance() + return getPendingAnswerWithoutError( + retrieveAnswerHandler( + getAnswerItemList() + ) + ) ?: UserAnswer.getDefaultInstance() } private fun getPendingAnswerWithoutError( - recyclerViewAssembler: StatePlayerRecyclerViewAssembler + answerHandler: InteractionAnswerHandler? ): UserAnswer? { - val items = if (isSplitView.get() == true) { - rightItemList - } else { - itemList - } - val answerHandler = recyclerViewAssembler - .getPendingAnswerHandler(items) return if (answerHandler?.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME) == null) { answerHandler?.getPendingAnswer() } else { null } } + + private fun getAnswerItemList(): List { + return if (isSplitView.get() == true) { + rightItemList + } else { + itemList + } + } } diff --git a/app/src/main/java/org/oppia/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt b/app/src/main/java/org/oppia/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt index 3882ad1c300..0c066dea9ad 100644 --- a/app/src/main/java/org/oppia/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/revision/revisionitemviewmodel/TopicRevisionItemViewModel.kt @@ -3,7 +3,6 @@ package org.oppia.app.topic.revision.revisionitemviewmodel import androidx.lifecycle.ViewModel import org.oppia.app.model.Subtopic import org.oppia.app.topic.revision.RevisionSubtopicSelector -import org.oppia.app.topic.revision.TopicRevisionFragment /** [ViewModel] for child views of recycler view present in the [TopicRevisionFragment]. */ class TopicRevisionItemViewModel( diff --git a/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardFragmentPresenter.kt b/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardFragmentPresenter.kt index 14da3f71517..189b3f7e186 100755 --- a/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardFragmentPresenter.kt @@ -33,9 +33,10 @@ class RevisionCardFragmentPresenter @Inject constructor( container, /* attachToRoot= */ false ) + val view = binding.revisionCardExplanationText val viewModel = getReviewCardViewModel() - viewModel.setSubtopicIdAndBinding(topicId, subtopicId, binding) + viewModel.setSubtopicIdAndBinding(topicId, subtopicId, view) logRevisionCardEvent(topicId, subtopicId) binding.let { diff --git a/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardViewModel.kt b/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardViewModel.kt index 3d028ad46d0..644582ef312 100755 --- a/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/revisioncard/RevisionCardViewModel.kt @@ -1,11 +1,11 @@ package org.oppia.app.topic.revisioncard import android.view.View +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel -import org.oppia.app.databinding.RevisionCardFragmentBinding import org.oppia.app.fragment.FragmentScope import org.oppia.app.model.RevisionCard import org.oppia.domain.topic.TopicController @@ -16,6 +16,7 @@ import org.oppia.util.parser.HtmlParser import org.oppia.util.parser.TopicHtmlParserEntityType import javax.inject.Inject +// TODO(#1633): Fix ViewModel to not depend on View /** [ViewModel] for revision card, providing rich text and worked examples */ @FragmentScope class RevisionCardViewModel @Inject constructor( @@ -28,7 +29,8 @@ class RevisionCardViewModel @Inject constructor( ) : ViewModel() { private lateinit var topicId: String private var subtopicId: Int = 0 - private lateinit var binding: RevisionCardFragmentBinding + private lateinit var view: TextView + private val returnToTopicClickListener: ReturnToTopicClickListener = activity as ReturnToTopicClickListener @@ -44,11 +46,11 @@ class RevisionCardViewModel @Inject constructor( fun setSubtopicIdAndBinding( topicId: String, subtopicId: Int, - binding: RevisionCardFragmentBinding + view: TextView ) { this.topicId = topicId this.subtopicId = subtopicId - this.binding = binding + this.view = view } private val revisionCardResultLiveData: LiveData> by lazy { @@ -73,7 +75,8 @@ class RevisionCardViewModel @Inject constructor( RevisionCard.getDefaultInstance() ) return htmlParserFactory.create( + resourceBucketName, entityType, topicId, /* imageCenterAlign= */ true - ).parseOppiaHtml(revisionCard.pageContents.html, binding.revisionCardExplanationText) + ).parseOppiaHtml(revisionCard.pageContents.html, view) } } diff --git a/app/src/main/java/org/oppia/app/utility/ClickableAreasImage.kt b/app/src/main/java/org/oppia/app/utility/ClickableAreasImage.kt index 2fe85b8ddb3..8e47124d9de 100644 --- a/app/src/main/java/org/oppia/app/utility/ClickableAreasImage.kt +++ b/app/src/main/java/org/oppia/app/utility/ClickableAreasImage.kt @@ -9,6 +9,7 @@ import androidx.core.view.isVisible import org.oppia.app.R import org.oppia.app.model.ImageWithRegions import org.oppia.app.player.state.ImageRegionSelectionInteractionView +import org.oppia.app.shim.ViewBindingShim import kotlin.math.roundToInt /** @@ -17,7 +18,8 @@ import kotlin.math.roundToInt class ClickableAreasImage( private val imageView: ImageRegionSelectionInteractionView, private val parentView: FrameLayout, - private val listener: OnClickableAreaClickedListener + private val listener: OnClickableAreaClickedListener, + private val bindingInterface: ViewBindingShim ) { init { imageView.setOnTouchListener { view, motionEvent -> @@ -27,6 +29,7 @@ class ClickableAreasImage( return@setOnTouchListener false } } + /** * Called when an image is clicked. * @@ -38,7 +41,7 @@ class ClickableAreasImage( // Show default region for non-accessibility cases and this will be only called when user taps on unspecified region. if (!imageView.isAccessibilityEnabled()) { resetRegionSelectionViews() - val defaultRegion = parentView.findViewById(R.id.default_selected_region) + val defaultRegion = bindingInterface.getDefaultRegion(parentView) defaultRegion.setBackgroundResource(R.drawable.selected_region_background) defaultRegion.x = x defaultRegion.y = y @@ -107,7 +110,7 @@ class ClickableAreasImage( } if (imageView.isAccessibilityEnabled()) { // Make default region visibility gone when talkback enabled to avoid any accidental touch. - val defaultRegion = parentView.findViewById(R.id.default_selected_region) + val defaultRegion = bindingInterface.getDefaultRegion(parentView) defaultRegion.isVisible = false newView.setOnClickListener { showOrHideRegion(newView, clickableArea) diff --git a/app/src/main/java/org/oppia/app/view/ViewComponent.kt b/app/src/main/java/org/oppia/app/view/ViewComponent.kt index d31e5702813..18db73bdd04 100644 --- a/app/src/main/java/org/oppia/app/view/ViewComponent.kt +++ b/app/src/main/java/org/oppia/app/view/ViewComponent.kt @@ -7,6 +7,7 @@ import org.oppia.app.customview.LessonThumbnailImageView import org.oppia.app.player.state.DragDropSortInteractionView import org.oppia.app.player.state.ImageRegionSelectionInteractionView import org.oppia.app.player.state.SelectionInteractionView +import org.oppia.app.profile.ProfileInputView /** Root subcomponent for custom views. */ @Subcomponent @@ -23,5 +24,6 @@ interface ViewComponent { fun inject(selectionInteractionView: SelectionInteractionView) fun inject(dragDropSortInteractionView: DragDropSortInteractionView) fun inject(imageRegionSelectionInteractionView: ImageRegionSelectionInteractionView) + fun inject(profileInputView: ProfileInputView) fun inject(lessonThumbnailImageView: LessonThumbnailImageView) } diff --git a/app/src/main/res/layout-land/selection_interaction_item.xml b/app/src/main/res/layout-land/selection_interaction_item.xml index 0ce0352db23..f6ed2067455 100644 --- a/app/src/main/res/layout-land/selection_interaction_item.xml +++ b/app/src/main/res/layout-land/selection_interaction_item.xml @@ -6,7 +6,7 @@ - + - + - + - +