diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentPresenter.kt index e66d0a1ea0b..7012c75a6bd 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentPresenter.kt @@ -31,7 +31,8 @@ import javax.inject.Inject @FragmentScope class AdministratorControlsFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, - private val fragment: Fragment + private val fragment: Fragment, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private lateinit var binding: AdministratorControlsFragmentBinding private lateinit var linearLayoutManager: LinearLayoutManager @@ -77,8 +78,8 @@ class AdministratorControlsFragmentPresenter @Inject constructor( /** Returns the recycler view adapter for the controls panel in administrator controls fragment. */ private fun createRecyclerViewAdapter(isMultipane: Boolean): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> + return multiTypeBuilderFactory + .create { viewModel -> viewModel.isMultipane.set(isMultipane) when (viewModel) { is AdministratorControlsGeneralViewModel -> { diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdFragmentPresenter.kt index de87f8773e6..e75218bb232 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/learneranalytics/ProfileAndDeviceIdFragmentPresenter.kt @@ -16,8 +16,10 @@ import javax.inject.Inject /** Presenter for arranging [ProfileAndDeviceIdFragment]'s UI. */ class ProfileAndDeviceIdFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val profileListViewModelFactory: ProfileListViewModel.Factory + private val profileListViewModelFactory: ProfileListViewModel.Factory, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { + private lateinit var binding: ProfileAndDeviceIdFragmentBinding /** Handles [ProfileAndDeviceIdFragment]'s creation flow. */ @@ -38,16 +40,15 @@ class ProfileAndDeviceIdFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is DeviceIdItemViewModel -> ProfileListItemViewType.DEVICE_ID - is ProfileLearnerIdItemViewModel -> ProfileListItemViewType.LEARNER_ID - is SyncStatusItemViewModel -> ProfileListItemViewType.SYNC_STATUS - else -> error("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is DeviceIdItemViewModel -> ProfileListItemViewType.DEVICE_ID + is ProfileLearnerIdItemViewModel -> ProfileListItemViewType.LEARNER_ID + is SyncStatusItemViewModel -> ProfileListItemViewType.SYNC_STATUS + else -> error("Encountered unexpected view model: $viewModel") } - .setLifecycleOwner(fragment) + } .registerViewDataBinder( viewType = ProfileListItemViewType.DEVICE_ID, inflateDataBinding = ProfileListDeviceIdItemBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt index 660113abfff..0d6dd2eb221 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt @@ -17,7 +17,8 @@ import javax.inject.Inject class CompletedStoryListFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var binding: CompletedStoryListFragmentBinding @@ -52,8 +53,7 @@ class CompletedStoryListFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = CompletedStoryItemBinding::inflate, setViewModel = CompletedStoryItemBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt index 5fab6646fa4..1b280beb5ef 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentPresenter.kt @@ -24,7 +24,8 @@ import javax.inject.Inject @FragmentScope class DeveloperOptionsFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, - private val fragment: Fragment + private val fragment: Fragment, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private lateinit var binding: DeveloperOptionsFragmentBinding @@ -59,28 +60,27 @@ class DeveloperOptionsFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is DeveloperOptionsModifyLessonProgressViewModel -> { - viewModel.itemIndex.set(0) - ViewType.VIEW_TYPE_MODIFY_LESSON_PROGRESS - } - is DeveloperOptionsViewLogsViewModel -> { - viewModel.itemIndex.set(1) - ViewType.VIEW_TYPE_VIEW_LOGS - } - is DeveloperOptionsOverrideAppBehaviorsViewModel -> { - viewModel.itemIndex.set(2) - ViewType.VIEW_TYPE_OVERRIDE_APP_BEHAVIORS - } - is DeveloperOptionsTestParsersViewModel -> { - viewModel.itemIndex.set(3) - ViewType.VIEW_TYPE_TEST_PARSERS - } - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is DeveloperOptionsModifyLessonProgressViewModel -> { + viewModel.itemIndex.set(0) + ViewType.VIEW_TYPE_MODIFY_LESSON_PROGRESS } + is DeveloperOptionsViewLogsViewModel -> { + viewModel.itemIndex.set(1) + ViewType.VIEW_TYPE_VIEW_LOGS + } + is DeveloperOptionsOverrideAppBehaviorsViewModel -> { + viewModel.itemIndex.set(2) + ViewType.VIEW_TYPE_OVERRIDE_APP_BEHAVIORS + } + is DeveloperOptionsTestParsersViewModel -> { + viewModel.itemIndex.set(3) + ViewType.VIEW_TYPE_TEST_PARSERS + } + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_MODIFY_LESSON_PROGRESS, inflateDataBinding = DeveloperOptionsModifyLessonProgressViewBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentPresenter.kt index c45d7e2d71d..1b09b469bb4 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentPresenter.kt @@ -21,7 +21,8 @@ class ForceNetworkTypeFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val networkConnectionUtil: Optional, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var binding: ForceNetworkTypeFragmentBinding @@ -60,8 +61,7 @@ class ForceNetworkTypeFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = ForceNetworkTypeNetworkItemViewBinding::inflate, setViewModel = this::bindNetworkItemView diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt index 8458664151b..6c9e67f565f 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentPresenter.kt @@ -22,7 +22,8 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val viewModel: MarkChaptersCompletedViewModel, - private val modifyLessonProgressController: ModifyLessonProgressController + private val modifyLessonProgressController: ModifyLessonProgressController, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) : ChapterSelector { private lateinit var binding: MarkChaptersCompletedFragmentBinding private lateinit var linearLayoutManager: LinearLayoutManager @@ -92,14 +93,14 @@ class MarkChaptersCompletedFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is StorySummaryViewModel -> ViewType.VIEW_TYPE_STORY - is ChapterSummaryViewModel -> ViewType.VIEW_TYPE_CHAPTER - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is StorySummaryViewModel -> ViewType.VIEW_TYPE_STORY + is ChapterSummaryViewModel -> ViewType.VIEW_TYPE_CHAPTER + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_STORY, inflateDataBinding = MarkChaptersCompletedStorySummaryViewBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt index cd98130c088..888d8929c2c 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedFragmentPresenter.kt @@ -20,7 +20,8 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val viewModel: MarkStoriesCompletedViewModel, - private val modifyLessonProgressController: ModifyLessonProgressController + private val modifyLessonProgressController: ModifyLessonProgressController, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) : StorySelector { private lateinit var binding: MarkStoriesCompletedFragmentBinding private lateinit var linearLayoutManager: LinearLayoutManager @@ -91,8 +92,7 @@ class MarkStoriesCompletedFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = MarkStoriesCompletedStorySummaryViewBinding::inflate, setViewModel = this::bindStorySummaryView diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt index 4c57fb63da8..2323036ccb8 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedFragmentPresenter.kt @@ -20,7 +20,8 @@ class MarkTopicsCompletedFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val viewModel: MarkTopicsCompletedViewModel, - private val modifyLessonProgressController: ModifyLessonProgressController + private val modifyLessonProgressController: ModifyLessonProgressController, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) : TopicSelector { private lateinit var binding: MarkTopicsCompletedFragmentBinding private lateinit var linearLayoutManager: LinearLayoutManager @@ -88,8 +89,7 @@ class MarkTopicsCompletedFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = MarkTopicsCompletedTopicViewBinding::inflate, setViewModel = this::bindTopicSummaryView diff --git a/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsFragmentPresenter.kt index e832d184378..9fe0b2849f8 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/vieweventlogs/ViewEventLogsFragmentPresenter.kt @@ -18,7 +18,8 @@ import javax.inject.Inject class ViewEventLogsFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var binding: ViewEventLogsFragmentBinding @@ -56,8 +57,7 @@ class ViewEventLogsFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = ViewEventLogsEventLogItemViewBinding::inflate, setViewModel = ViewEventLogsEventLogItemViewBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt index 4ff8411e9d8..5446a1d55ed 100644 --- a/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt +++ b/app/src/main/java/org/oppia/android/app/fragment/FragmentComponentImpl.kt @@ -64,6 +64,7 @@ import org.oppia.android.app.settings.profile.ProfileResetPinFragment import org.oppia.android.app.shim.IntentFactoryShimModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.story.StoryFragment +import org.oppia.android.app.testing.DragDropTestFragment import org.oppia.android.app.testing.ExplorationTestActivityPresenter import org.oppia.android.app.testing.ImageRegionSelectionTestFragment import org.oppia.android.app.topic.TopicFragment @@ -114,6 +115,7 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(conceptCardFragment: ConceptCardFragment) fun inject(developerOptionsFragment: DeveloperOptionsFragment) fun inject(downloadsTabFragment: DownloadsTabFragment) + fun inject(dragDropTestFragment: DragDropTestFragment) fun inject(exitProfileDialogFragment: ExitProfileDialogFragment) fun inject(explorationFragment: ExplorationFragment) fun inject(explorationManagerFragment: ExplorationManagerFragment) diff --git a/app/src/main/java/org/oppia/android/app/help/HelpFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/help/HelpFragmentPresenter.kt index 19c07875867..2748b323c4f 100644 --- a/app/src/main/java/org/oppia/android/app/help/HelpFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/HelpFragmentPresenter.kt @@ -18,7 +18,8 @@ import javax.inject.Inject class HelpFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var binding: HelpFragmentBinding @@ -48,8 +49,7 @@ class HelpFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = HelpItemBinding::inflate, setViewModel = HelpItemBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/help/faq/FAQListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/help/faq/FAQListFragmentPresenter.kt index 13a4f16c30f..ca0288a22bd 100644 --- a/app/src/main/java/org/oppia/android/app/help/faq/FAQListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/faq/FAQListFragmentPresenter.kt @@ -22,7 +22,8 @@ import javax.inject.Inject class FAQListFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private lateinit var binding: FaqListFragmentBinding @@ -48,14 +49,13 @@ class FAQListFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is FAQHeaderViewModel -> ViewType.VIEW_TYPE_HEADER - is FAQContentViewModel -> ViewType.VIEW_TYPE_CONTENT - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is FAQHeaderViewModel -> ViewType.VIEW_TYPE_HEADER + is FAQContentViewModel -> ViewType.VIEW_TYPE_CONTENT + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_HEADER, inflateDataBinding = FaqItemHeaderBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListFragmentPresenter.kt index 653c69f33f6..2970d448908 100644 --- a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseListFragmentPresenter.kt @@ -18,7 +18,8 @@ import javax.inject.Inject class LicenseListFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var binding: LicenseListFragmentBinding @@ -51,8 +52,7 @@ class LicenseListFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = LicenseItemBinding::inflate, setViewModel = LicenseItemBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListFragmentPresenter.kt index c7a604729a0..50fa6374c40 100644 --- a/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/help/thirdparty/ThirdPartyDependencyListFragmentPresenter.kt @@ -18,7 +18,8 @@ import javax.inject.Inject class ThirdPartyDependencyListFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var binding: ThirdPartyDependencyListFragmentBinding @@ -50,8 +51,7 @@ class ThirdPartyDependencyListFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = ThirdPartyDependencyItemBinding::inflate, setViewModel = ThirdPartyDependencyItemBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt index 32410bd2a0c..d2b2cb0d5ab 100644 --- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt @@ -35,7 +35,8 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( private val htmlParserFactory: HtmlParser.Factory, @DefaultResourceBucketName private val resourceBucketName: String, @ExplorationHtmlParserEntityType private val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private var index: Int? = null @@ -162,34 +163,29 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is HintsViewModel -> ViewType.VIEW_TYPE_HINT_ITEM - is SolutionViewModel -> ViewType.VIEW_TYPE_SOLUTION_ITEM - is ReturnToLessonViewModel -> ViewType.VIEW_TYPE_RETURN_TO_LESSON_ITEM - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is HintsViewModel -> ViewType.VIEW_TYPE_HINT_ITEM + is SolutionViewModel -> ViewType.VIEW_TYPE_SOLUTION_ITEM + is ReturnToLessonViewModel -> ViewType.VIEW_TYPE_RETURN_TO_LESSON_ITEM + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } - .registerViewDataBinder( - viewType = ViewType.VIEW_TYPE_HINT_ITEM, - inflateDataBinding = HintsSummaryBinding::inflate, - setViewModel = this::bindHintsViewModel, - transformViewModel = { it as HintsViewModel } - ) - .registerViewDataBinder( - viewType = ViewType.VIEW_TYPE_SOLUTION_ITEM, - inflateDataBinding = SolutionSummaryBinding::inflate, - setViewModel = this::bindSolutionViewModel, - transformViewModel = { it as SolutionViewModel } - ) - .registerViewDataBinder( - viewType = ViewType.VIEW_TYPE_RETURN_TO_LESSON_ITEM, - inflateDataBinding = ReturnToLessonButtonItemBinding::inflate, - setViewModel = this::bindReturnToLessonViewModel, - transformViewModel = { it as ReturnToLessonViewModel } - ) - .build() + }.registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_HINT_ITEM, + inflateDataBinding = HintsSummaryBinding::inflate, + setViewModel = this::bindHintsViewModel, + transformViewModel = { it as HintsViewModel } + ).registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_SOLUTION_ITEM, + inflateDataBinding = SolutionSummaryBinding::inflate, + setViewModel = this::bindSolutionViewModel, + transformViewModel = { it as SolutionViewModel } + ).registerViewDataBinder( + viewType = ViewType.VIEW_TYPE_RETURN_TO_LESSON_ITEM, + inflateDataBinding = ReturnToLessonButtonItemBinding::inflate, + setViewModel = this::bindReturnToLessonViewModel, + transformViewModel = { it as ReturnToLessonViewModel } + ).build() } private fun bindHintsViewModel( diff --git a/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt index 7f196cd9dc2..ae37501e728 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt @@ -43,7 +43,8 @@ class HomeFragmentPresenter @Inject constructor( @StoryHtmlParserEntityType private val storyEntityType: String, private val resourceHandler: AppLanguageResourceHandler, private val dateTimeUtil: DateTimeUtil, - private val translationController: TranslationController + private val translationController: TranslationController, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private val routeToTopicPlayStoryListener = activity as RouteToTopicPlayStoryListener private lateinit var binding: HomeFragmentBinding @@ -96,17 +97,16 @@ class HomeFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is WelcomeViewModel -> ViewType.WELCOME_MESSAGE - is PromotedStoryListViewModel -> ViewType.PROMOTED_STORY_LIST - is ComingSoonTopicListViewModel -> ViewType.COMING_SOON_TOPIC_LIST - is AllTopicsViewModel -> ViewType.ALL_TOPICS - is TopicSummaryViewModel -> ViewType.TOPIC_LIST - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is WelcomeViewModel -> ViewType.WELCOME_MESSAGE + is PromotedStoryListViewModel -> ViewType.PROMOTED_STORY_LIST + is ComingSoonTopicListViewModel -> ViewType.COMING_SOON_TOPIC_LIST + is AllTopicsViewModel -> ViewType.ALL_TOPICS + is TopicSummaryViewModel -> ViewType.TOPIC_LIST + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.WELCOME_MESSAGE, inflateDataBinding = WelcomeBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsListView.kt b/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsListView.kt index 3bc04b2cd25..9a2e13f4808 100644 --- a/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsListView.kt +++ b/app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicsListView.kt @@ -32,6 +32,11 @@ class ComingSoonTopicsListView @JvmOverloads constructor( @Inject lateinit var oppiaLogger: OppiaLogger + @Inject + lateinit var singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory + + private lateinit var comingSoonDataList: List + override fun onAttachedToWindow() { super.onAttachedToWindow() @@ -45,11 +50,22 @@ class ComingSoonTopicsListView @JvmOverloads constructor( val snapHelper = StartSnapHelper() onFlingListener = null snapHelper.attachToRecyclerView(this) + maybeInitializeAdapter() + } + + private fun maybeInitializeAdapter() { + if (::bindingInterface.isInitialized && + ::bindingInterface.isInitialized && + ::oppiaLogger.isInitialized && + ::singleTypeBuilderFactory.isInitialized && + ::comingSoonDataList.isInitialized + ) { + bindDataToAdapter() + } } /** * Sets the list of coming soon topics that this view shows to the learner. - * * @param newDataList the new list of topics to present */ fun setComingSoonTopicList(newDataList: List?) { @@ -58,18 +74,29 @@ class ComingSoonTopicsListView @JvmOverloads constructor( // way to check that the adapter is created. // This ensures that the adapter will only be created once and correctly rebinds the data. // For more context: https://github.com/oppia/oppia-android/pull/2246#pullrequestreview-565964462 - if (adapter == null) { - adapter = createAdapter() - } if (newDataList == null) { - oppiaLogger.w(COMING_SOON_TOPIC_LIST_VIEW_TAG, "Failed to resolve upcoming topic list data") + oppiaLogger.w(COMING_SOON_TOPIC_LIST_VIEW_TAG, "Failed to resolve new topics list data") } else { - (adapter as BindableAdapter<*>).setDataUnchecked(newDataList) + comingSoonDataList = newDataList + maybeInitializeAdapter() } } + private fun bindDataToAdapter() { + // To reliably bind data only after the adapter is created, we manually set the data so we can first + // check for the adapter; when using an existing [RecyclerViewBindingAdapter] there is no reliable + // way to check that the adapter is created. + // This ensures that the adapter will only be created once and correctly rebinds the data. + // For more context: https://github.com/oppia/oppia-android/pull/2246#pullrequestreview-565964462 + if (adapter == null) { + adapter = createAdapter() + } + + (adapter as BindableAdapter<*>).setDataUnchecked(comingSoonDataList) + } + private fun createAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder.newBuilder() + return singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> bindingInterface.provideComingSoonTopicViewInflatedView( diff --git a/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryListView.kt b/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryListView.kt index 6b65159b727..74467bff9a1 100644 --- a/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryListView.kt +++ b/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryListView.kt @@ -32,9 +32,13 @@ class PromotedStoryListView @JvmOverloads constructor( @Inject lateinit var oppiaLogger: OppiaLogger + @Inject + lateinit var singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory + + private lateinit var promotedDataList: List + override fun onAttachedToWindow() { super.onAttachedToWindow() - val viewComponentFactory = FragmentManager.findFragment(this) as ViewComponentFactory val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl viewComponent.inject(this) @@ -45,14 +49,34 @@ class PromotedStoryListView @JvmOverloads constructor( val snapHelper = StartSnapHelper() onFlingListener = null snapHelper.attachToRecyclerView(this) + maybeInitializeAdapter() + } + + private fun maybeInitializeAdapter() { + if (::bindingInterface.isInitialized && + ::bindingInterface.isInitialized && + ::oppiaLogger.isInitialized && + ::singleTypeBuilderFactory.isInitialized && + ::promotedDataList.isInitialized + ) { + bindDataToAdapter() + } } /** * Sets the list of promoted stories that this view shows to the learner. - * * @param newDataList the new list of stories to present */ fun setPromotedStoryList(newDataList: List?) { + if (newDataList == null) { + oppiaLogger.w(PROMOTED_STORY_LIST_VIEW_TAG, "Failed to resolve new topics list data") + } else { + promotedDataList = newDataList + maybeInitializeAdapter() + } + } + + private fun bindDataToAdapter() { // To reliably bind data only after the adapter is created, we manually set the data so we can first // check for the adapter; when using an existing [RecyclerViewBindingAdapter] there is no reliable // way to check that the adapter is created. @@ -61,15 +85,12 @@ class PromotedStoryListView @JvmOverloads constructor( if (adapter == null) { adapter = createAdapter() } - if (newDataList == null) { - oppiaLogger.w(PROMOTED_STORY_LIST_VIEW_TAG, "Failed to resolve new story list data") - } else { - (adapter as BindableAdapter<*>).setDataUnchecked(newDataList) - } + + (adapter as BindableAdapter<*>).setDataUnchecked(promotedDataList) } private fun createAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder.newBuilder() + return singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> bindingInterface.providePromotedStoryCardInflatedView( diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt index 04b07ccedc8..4fc99927934 100644 --- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt @@ -31,7 +31,8 @@ class OnboardingFragmentPresenter @Inject constructor( private val viewModelProvider: ViewModelProvider, private val viewModelProviderFinalSlide: ViewModelProvider, private val resourceHandler: AppLanguageResourceHandler, - private val htmlParserFactory: HtmlParser.Factory + private val htmlParserFactory: HtmlParser.Factory, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) : OnboardingNavigationListener, HtmlParser.PolicyOppiaTagActionListener { private val dotsList = ArrayList() private lateinit var binding: OnboardingFragmentBinding @@ -100,14 +101,13 @@ class OnboardingFragmentPresenter @Inject constructor( } private fun createViewPagerAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is OnboardingSlideViewModel -> ViewType.ONBOARDING_MIDDLE_SLIDE - is OnboardingSlideFinalViewModel -> ViewType.ONBOARDING_FINAL_SLIDE - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is OnboardingSlideViewModel -> ViewType.ONBOARDING_MIDDLE_SLIDE + is OnboardingSlideFinalViewModel -> ViewType.ONBOARDING_FINAL_SLIDE + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.ONBOARDING_MIDDLE_SLIDE, inflateDataBinding = OnboardingSlideBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListFragmentPresenter.kt index dafe1d384f3..09968096fad 100644 --- a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListFragmentPresenter.kt @@ -17,7 +17,8 @@ import javax.inject.Inject class OngoingTopicListFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var binding: OngoingTopicListFragmentBinding @@ -56,8 +57,7 @@ class OngoingTopicListFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = OngoingTopicItemBinding::inflate, setViewModel = OngoingTopicItemBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt index 89810a9266f..2cf5e808f1a 100644 --- a/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AppLanguageFragmentPresenter.kt @@ -12,7 +12,8 @@ import javax.inject.Inject /** The presenter for [AppLanguageFragment]. */ class AppLanguageFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val appLanguageSelectionViewModel: AppLanguageSelectionViewModel + private val appLanguageSelectionViewModel: AppLanguageSelectionViewModel, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private lateinit var prefSummaryValue: String fun handleOnCreateView( @@ -41,9 +42,7 @@ class AppLanguageFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() - .setLifecycleOwner(fragment) + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = AppLanguageItemBinding::inflate, setViewModel = AppLanguageItemBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt index f235685035f..5195adcebe1 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt @@ -13,7 +13,8 @@ import javax.inject.Inject /** The presenter for [AudioLanguageFragment]. */ class AudioLanguageFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel + private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { /** * Returns a newly inflated view to render the fragment with the specified [audioLanguage] as the @@ -44,9 +45,7 @@ class AudioLanguageFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() - .setLifecycleOwner(fragment) + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = AudioLanguageItemBinding::inflate, setViewModel = AudioLanguageItemBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt index 86978573d83..14fe99b0c32 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt @@ -44,7 +44,8 @@ class OptionsFragmentPresenter @Inject constructor( private val fragment: Fragment, private val profileManagementController: ProfileManagementController, private val viewModelProvider: ViewModelProvider, - private val oppiaLogger: OppiaLogger + private val oppiaLogger: OppiaLogger, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private lateinit var binding: OptionsFragmentBinding private lateinit var recyclerViewAdapter: RecyclerView.Adapter<*> @@ -91,25 +92,24 @@ class OptionsFragmentPresenter @Inject constructor( private fun createRecyclerViewAdapter( isMultipane: Boolean ): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - viewModel.isMultipane.set(isMultipane) - when (viewModel) { - is OptionsReadingTextSizeViewModel -> { - viewModel.itemIndex.set(0) - ViewType.VIEW_TYPE_READING_TEXT_SIZE - } - is OptionsAppLanguageViewModel -> { - viewModel.itemIndex.set(1) - ViewType.VIEW_TYPE_APP_LANGUAGE - } - is OptionsAudioLanguageViewModel -> { - viewModel.itemIndex.set(2) - ViewType.VIEW_TYPE_AUDIO_LANGUAGE - } - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") + return multiTypeBuilderFactory.create { viewModel -> + viewModel.isMultipane.set(isMultipane) + when (viewModel) { + is OptionsReadingTextSizeViewModel -> { + viewModel.itemIndex.set(0) + ViewType.VIEW_TYPE_READING_TEXT_SIZE } + is OptionsAppLanguageViewModel -> { + viewModel.itemIndex.set(1) + ViewType.VIEW_TYPE_APP_LANGUAGE + } + is OptionsAudioLanguageViewModel -> { + viewModel.itemIndex.set(2) + ViewType.VIEW_TYPE_AUDIO_LANGUAGE + } + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_READING_TEXT_SIZE, inflateDataBinding = OptionStoryTextSizeBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt index 1331a2577c3..0af396653ea 100644 --- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeFragmentPresenter.kt @@ -13,7 +13,8 @@ import javax.inject.Inject /** The presenter for [ReadingTextSizeFragment]. */ class ReadingTextSizeFragmentPresenter @Inject constructor( private val fragment: Fragment, - private val readingTextSizeSelectionViewModel: ReadingTextSizeSelectionViewModel + private val readingTextSizeSelectionViewModel: ReadingTextSizeSelectionViewModel, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { fun handleOnCreateView( inflater: LayoutInflater, @@ -39,13 +40,12 @@ class ReadingTextSizeFragmentPresenter @Inject constructor( fun getTextSizeSelected(): ReadingTextSize? = readingTextSizeSelectionViewModel.selectedTextSize private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() - .setLifecycleOwner(fragment) + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = TextSizeItemsBinding::inflate, setViewModel = TextSizeItemsBinding::setViewModel - ).build() + ) + .build() } private fun updateTextSize(textSize: ReadingTextSize) { diff --git a/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt b/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt index bb42f0ba1a3..706c3f57013 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/DragDropSortInteractionView.kt @@ -4,7 +4,6 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import androidx.core.view.isVisible -import androidx.databinding.BindingAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.ItemTouchHelper @@ -25,35 +24,25 @@ import javax.inject.Inject /** * A custom [RecyclerView] for displaying a list of items that can be re-ordered using - * [DragItemTouchHelperCallback]. + * [DragAndDropItemFacilitator]. */ class DragDropSortInteractionView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : RecyclerView(context, attrs, defStyleAttr) { - // For disabling grouping of items by default. - private var isMultipleItemsInSamePositionAllowed: Boolean = false - private var isAccessibilityEnabled: Boolean = false + @field:[Inject ExplorationHtmlParserEntityType] lateinit var entityType: String + @field:[Inject DefaultResourceBucketName] lateinit var resourceBucketName: String - @Inject - lateinit var htmlParserFactory: HtmlParser.Factory - - @Inject - lateinit var accessibilityService: AccessibilityService - - @Inject - @field:ExplorationHtmlParserEntityType - lateinit var entityType: String - - @Inject - @field:DefaultResourceBucketName - lateinit var resourceBucketName: String - - @Inject - lateinit var viewBindingShim: ViewBindingShim + @Inject lateinit var htmlParserFactory: HtmlParser.Factory + @Inject lateinit var accessibilityService: AccessibilityService + @Inject lateinit var viewBindingShim: ViewBindingShim + @Inject lateinit var singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory + private var multipleItemsInSamePositionInitialized = false + private var isMultipleItemsInSamePositionAllowed: Boolean? = null private lateinit var entityId: String + private lateinit var dataList: List private lateinit var onDragEnd: OnDragEndedListener private lateinit var onItemDrag: OnItemDragListener @@ -63,28 +52,67 @@ class DragDropSortInteractionView @JvmOverloads constructor( val viewComponentFactory = FragmentManager.findFragment(this) as ViewComponentFactory val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl viewComponent.inject(this) - - isAccessibilityEnabled = accessibilityService.isScreenReaderEnabled() + maybeInitializeAdapter() } - fun allowMultipleItemsInSamePosition(isAllowed: Boolean) { - // TODO(#299): Find a cleaner way to initialize the item input type. Using data-binding results in a race condition - // with setting the adapter data, so this needs to be done in an order-agnostic way. There should be a way to do - // this more efficiently and cleanly than always relying on notifying of potential changes in the adapter when the - // type is set (plus the type ought to be permanent). + fun setAllowMultipleItemsInSamePosition(isAllowed: Boolean) { this.isMultipleItemsInSamePositionAllowed = isAllowed - adapter = createAdapter() + multipleItemsInSamePositionInitialized = true + maybeInitializeAdapter() } - // TODO(#264): Clean up HTML parser such that it can be handled completely through a binding adapter, allowing - // TextViews that require custom Oppia HTML parsing to be fully automatically bound through data-binding. + // TODO(#264): Clean up HTML parser such that it can be handled completely through a binding + // adapter, allowing TextViews that require custom Oppia HTML parsing to be fully automatically + // bound through data-binding. fun setEntityId(entityId: String) { this.entityId = entityId + maybeInitializeAdapter() + } + + /** + * Sets the view's RecyclerView [DragDropInteractionContentViewModel] data list. + * + * Note that this needs to be used instead of the generic RecyclerView 'data' binding adapter + * since this one takes into account initialization order with other binding properties. + */ + fun setDraggableData(dataList: List) { + this.dataList = dataList + maybeInitializeAdapter() + } + + fun setOnDragEnded(onDragEnd: OnDragEndedListener) { + this.onDragEnd = onDragEnd + maybeAttachItemTouchHelper() + } + + fun setOnItemDrag(onItemDrag: OnItemDragListener) { + this.onItemDrag = onItemDrag + maybeAttachItemTouchHelper() } - private fun createAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + private fun maybeInitializeAdapter() { + val itemsInSamePositionAllowed = isMultipleItemsInSamePositionAllowed + if (::singleTypeBuilderFactory.isInitialized && + multipleItemsInSamePositionInitialized && + ::entityId.isInitialized && + ::dataList.isInitialized && + itemsInSamePositionAllowed != null + ) { + adapter = createAdapter(itemsInSamePositionAllowed).also { it.setData(dataList) } + } + } + + private fun maybeAttachItemTouchHelper() { + if (::onDragEnd.isInitialized && ::onItemDrag.isInitialized) { + val itemTouchHelper = ItemTouchHelper(DragAndDropItemFacilitator(onItemDrag, onDragEnd)) + itemTouchHelper.attachToRecyclerView(this) + } + } + + private fun createAdapter( + itemsInSamePositionAllowed: Boolean + ): BindableAdapter { + return singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> viewBindingShim.provideDragDropSortInteractionInflatedView( @@ -99,11 +127,11 @@ class DragDropSortInteractionView @JvmOverloads constructor( createNestedAdapter() adapter?.let { viewBindingShim.setDragDropInteractionItemsBindingAdapter(it) } viewBindingShim.getDragDropInteractionItemsBindingGroupItem().isVisible = - isMultipleItemsInSamePositionAllowed + itemsInSamePositionAllowed viewBindingShim.getDragDropInteractionItemsBindingUnlinkItems().isVisible = viewModel.htmlContent.contentIdsList.size > 1 viewBindingShim.getDragDropInteractionItemsBindingAccessibleContainer().isVisible = - isAccessibilityEnabled + accessibilityService.isScreenReaderEnabled() viewBindingShim.setDragDropInteractionItemsBindingViewModel(viewModel) } ) @@ -111,8 +139,7 @@ class DragDropSortInteractionView @JvmOverloads constructor( } private fun createNestedAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> viewBindingShim.provideDragDropSingleItemInflatedView( @@ -134,42 +161,4 @@ class DragDropSortInteractionView @JvmOverloads constructor( ) .build() } - - 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. */ -@BindingAdapter("entityId") -fun setEntityId( - dragDropSortInteractionView: DragDropSortInteractionView, - entityId: String -) = dragDropSortInteractionView.setEntityId(entityId) - -/** Sets the [SelectionItemInputType] for a specific [SelectionInteractionView] via data-binding. */ -@BindingAdapter("allowMultipleItemsInSamePosition") -fun setAllowMultipleItemsInSamePosition( - dragDropSortInteractionView: DragDropSortInteractionView, - isAllowed: Boolean -) = dragDropSortInteractionView.allowMultipleItemsInSamePosition(isAllowed) diff --git a/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt b/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt index 24d845c32c3..d94b853f1ef 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt @@ -25,7 +25,7 @@ import org.oppia.android.util.parser.image.ImageViewTarget import javax.inject.Inject /** - * A custom [AppCompatImageView] with a list of [LabeledRegion] to work with + * A custom [AppCompatImageView] with a list of [ImageWithRegions.LabeledRegion]s to work with * [ClickableAreasImage]. * * In order to correctly work with this interaction make sure you've called attached an listener @@ -36,67 +36,34 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatImageView(context, attrs, defStyleAttr) { + @field:[Inject ExplorationHtmlParserEntityType] lateinit var entityType: String + @field:[Inject DefaultResourceBucketName] lateinit var resourceBucketName: String + @field:[Inject ImageDownloadUrlTemplate] lateinit var imageDownloadUrlTemplate: String + @field:[Inject DefaultGcsPrefix] lateinit var gcsPrefix: String - private var isAccessibilityEnabled: Boolean = false - private lateinit var imageUrl: String - private var clickableAreas: List = emptyList() - private lateinit var listener: OnClickableAreaClickedListener - - @Inject - lateinit var accessibilityService: AccessibilityService - - @Inject - lateinit var imageLoader: ImageLoader - - @Inject - @field:ExplorationHtmlParserEntityType - lateinit var entityType: String - - @Inject - @field:DefaultResourceBucketName - lateinit var resourceBucketName: String - - @Inject - @field:ImageDownloadUrlTemplate - lateinit var imageDownloadUrlTemplate: String - - @Inject - @field:DefaultGcsPrefix - lateinit var gcsPrefix: String - - @Inject - lateinit var bindingInterface: ViewBindingShim - - @Inject - lateinit var machineLocale: OppiaLocale.MachineLocale + @Inject lateinit var bindingInterface: ViewBindingShim + @Inject lateinit var machineLocale: OppiaLocale.MachineLocale + @Inject lateinit var accessibilityService: AccessibilityService + @Inject lateinit var imageLoader: ImageLoader private lateinit var entityId: String private lateinit var overlayView: FrameLayout private lateinit var onRegionClicked: OnClickableAreaClickedListener + private lateinit var imageUrl: String + private lateinit var clickableAreas: List /** - * Sets the URL for the image & initiates loading it. This is intended to be called via data-binding. + * Sets the URL for the image & initiates loading it. This is intended to be called via + * data-binding. */ fun setImageUrl(imageUrl: String) { this.imageUrl = imageUrl - loadImage() - } - - /** Initiates the asynchronous loading process for the interaction's image region. */ - private fun loadImage() { - val imageName = machineLocale.run { - imageDownloadUrlTemplate.formatForMachines(entityType, entityId, imageUrl) - } - val imageUrl = "$gcsPrefix/$resourceBucketName/$imageName" - if (machineLocale.run { imageUrl.endsWithIgnoreCase("svg") }) { - imageLoader.loadBlockSvg(imageUrl, ImageViewTarget(this)) - } else { - imageLoader.loadBitmap(imageUrl, ImageViewTarget(this)) - } + maybeInitializeClickableAreas() } fun setEntityId(entityId: String) { this.entityId = entityId + maybeInitializeClickableAreas() } fun setClickableAreas(clickableAreas: List) { @@ -112,26 +79,7 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( } } } - } - - /** Binds [OnClickableAreaClickedListener] with the view inorder to get callback from [ClickableAreasImage]. */ - fun setListener(onClickableAreaClickedListener: OnClickableAreaClickedListener) { - this.listener = onClickableAreaClickedListener - val area = ClickableAreasImage( - this, - this.parent as FrameLayout, - listener, - bindingInterface - ) - area.addRegionViews() - } - - fun getClickableAreas(): List { - return clickableAreas - } - - fun isAccessibilityEnabled(): Boolean { - return isAccessibilityEnabled + maybeInitializeClickableAreas() } override fun onAttachedToWindow() { @@ -140,34 +88,56 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( val viewComponentFactory = FragmentManager.findFragment(this) as ViewComponentFactory val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl viewComponent.inject(this) - - isAccessibilityEnabled = accessibilityService.isScreenReaderEnabled() + maybeInitializeClickableAreas() } fun setOnRegionClicked(onRegionClicked: OnClickableAreaClickedListener) { this.onRegionClicked = onRegionClicked - checkIfSettingIsPossible() + maybeInitializeClickableAreas() } fun setOverlayView(overlayView: FrameLayout) { this.overlayView = overlayView - checkIfSettingIsPossible() + maybeInitializeClickableAreas() } - private fun checkIfSettingIsPossible() { - if (::onRegionClicked.isInitialized && ::overlayView.isInitialized) { - performAttachment() + private fun maybeInitializeClickableAreas() { + if (::accessibilityService.isInitialized && + ::clickableAreas.isInitialized && + ::entityId.isInitialized && + ::imageUrl.isInitialized && + ::onRegionClicked.isInitialized && + ::overlayView.isInitialized + ) { + loadImage() + + val areasImage = ClickableAreasImage( + this, + this.parent as FrameLayout, + onRegionClicked, + bindingInterface, + isAccessibilityEnabled = accessibilityService.isScreenReaderEnabled(), + clickableAreas + ) + areasImage.addRegionViews() + performAttachment(areasImage) } } - private fun performAttachment() { - val area = ClickableAreasImage( - this, - overlayView, - onRegionClicked, - bindingInterface - ) + /** Initiates the asynchronous loading process for the interaction's image region. */ + private fun loadImage() { + val imageName = machineLocale.run { + imageDownloadUrlTemplate.formatForMachines(entityType, entityId, imageUrl) + } + val imageUrl = "$gcsPrefix/$resourceBucketName/$imageName" + if (machineLocale.run { imageUrl.endsWithIgnoreCase("svg") }) { + imageLoader.loadBlockSvg(imageUrl, ImageViewTarget(this)) + } else { + imageLoader.loadBitmap(imageUrl, ImageViewTarget(this)) + } + } + private fun performAttachment(areasImage: ClickableAreasImage) { this.addOnLayoutChangeListener { _, left, @@ -179,8 +149,9 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor( oldRight, oldBottom -> // Update the regions, as the bounds have changed - if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) - area.addRegionViews() + if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { + areasImage.addRegionViews() + } } } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt b/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt index 0a0f06df55c..1265ed41808 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/SelectionInteractionView.kt @@ -3,7 +3,7 @@ package org.oppia.android.app.player.state import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater -import androidx.databinding.BindingAdapter +import androidx.databinding.ObservableList import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView @@ -20,55 +20,45 @@ import org.oppia.android.util.parser.html.HtmlParser import javax.inject.Inject /** - * 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. + * A custom [RecyclerView] for displaying a variable list of items that may be selected by a user as + * part of the item selection or multiple choice interactions. */ class SelectionInteractionView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : RecyclerView(context, attrs, defStyleAttr) { - // Default to checkboxes to ensure that something can render even if it may not be correct. - private var selectionItemInputType: SelectionItemInputType = SelectionItemInputType.CHECKBOXES + @field:[Inject ExplorationHtmlParserEntityType] lateinit var entityType: String + @field:[Inject DefaultResourceBucketName] lateinit var resourceBucketName: String - @Inject - lateinit var htmlParserFactory: HtmlParser.Factory - - @Inject - @field:ExplorationHtmlParserEntityType - lateinit var entityType: String - - @Inject - @field:DefaultResourceBucketName - lateinit var resourceBucketName: String - - @Inject - lateinit var bindingInterface: ViewBindingShim + @Inject lateinit var htmlParserFactory: HtmlParser.Factory + @Inject lateinit var bindingInterface: ViewBindingShim + @Inject lateinit var singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory + private lateinit var selectionItemInputType: SelectionItemInputType private lateinit var entityId: String private lateinit var writtenTranslationContext: WrittenTranslationContext + private lateinit var dataList: ObservableList override fun onAttachedToWindow() { super.onAttachedToWindow() - val viewComponentFactory = FragmentManager.findFragment(this) as ViewComponentFactory val viewComponent = viewComponentFactory.createViewComponent(this) as ViewComponentImpl viewComponent.inject(this) + maybeInitializeAdapter() } fun setAllOptionsItemInputType(selectionItemInputType: SelectionItemInputType) { - // TODO(#299): Find a cleaner way to initialize the item input type. Using data-binding results in a race condition - // with setting the adapter data, so this needs to be done in an order-agnostic way. There should be a way to do - // this more efficiently and cleanly than always relying on notifying of potential changes in the adapter when the - // type is set (plus the type ought to be permanent). this.selectionItemInputType = selectionItemInputType - adapter = createAdapter() + maybeInitializeAdapter() } - // TODO(#264): Clean up HTML parser such that it can be handled completely through a binding adapter, allowing - // TextViews that require custom Oppia HTML parsing to be fully automatically bound through data-binding. + // TODO(#264): Clean up HTML parser such that it can be handled completely through a binding + // adapter, allowing TextViews that require custom Oppia HTML parsing to be fully automatically + // bound through data-binding. fun setEntityId(entityId: String) { this.entityId = entityId + maybeInitializeAdapter() } /** @@ -78,13 +68,35 @@ class SelectionInteractionView @JvmOverloads constructor( */ fun setWrittenTranslationContext(writtenTranslationContext: WrittenTranslationContext) { this.writtenTranslationContext = writtenTranslationContext + maybeInitializeAdapter() + } + + /** + * Sets the view's RecyclerView [SelectionInteractionContentViewModel] data list. + * + * Note that this needs to be used instead of the generic RecyclerView 'data' binding adapter + * since this one takes into account initialization order with other binding properties. + */ + fun setSelectionData(dataList: ObservableList) { + this.dataList = dataList + maybeInitializeAdapter() + } + + private fun maybeInitializeAdapter() { + if (::singleTypeBuilderFactory.isInitialized && + ::selectionItemInputType.isInitialized && + ::entityId.isInitialized && + ::writtenTranslationContext.isInitialized && + ::dataList.isInitialized + ) { + adapter = createAdapter().also { it.setData(dataList) } + } } private fun createAdapter(): BindableAdapter { return when (selectionItemInputType) { SelectionItemInputType.CHECKBOXES -> - BindableAdapter.SingleTypeBuilder - .newBuilder() + singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> bindingInterface.provideSelectionInteractionViewInflatedView( @@ -107,8 +119,7 @@ class SelectionInteractionView @JvmOverloads constructor( ) .build() SelectionItemInputType.RADIO_BUTTONS -> - BindableAdapter.SingleTypeBuilder - .newBuilder() + singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> bindingInterface.provideMultipleChoiceInteractionItemsInflatedView( @@ -133,17 +144,3 @@ class SelectionInteractionView @JvmOverloads constructor( } } } - -/** Sets the exploration ID for a specific [SelectionInteractionView] via data-binding. */ -@BindingAdapter("entityId") -fun setEntityId( - selectionInteractionView: SelectionInteractionView, - entityId: String -) = selectionInteractionView.setEntityId(entityId) - -/** Sets the translation context for a specific [SelectionInteractionView] via data-binding. */ -@BindingAdapter("writtenTranslationContext") -fun setWrittenTranslationContext( - selectionInteractionView: SelectionInteractionView, - writtenTranslationContext: WrittenTranslationContext -) = selectionInteractionView.setWrittenTranslationContext(writtenTranslationContext) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 58453675b35..bac406d7fda 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -887,11 +887,13 @@ class StatePlayerRecyclerViewAssembler private constructor( private val interactionViewModelFactoryMap: Map, private val backgroundCoroutineDispatcher: CoroutineDispatcher, private val resourceHandler: AppLanguageResourceHandler, - private val translationController: TranslationController + private val translationController: TranslationController, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { - private val adapterBuilder = BindableAdapter.MultiTypeBuilder.newBuilder( - StateItemViewModel::viewType - ) + + private val adapterBuilder: BindableAdapter.MultiTypeBuilder = multiTypeBuilderFactory.create { it.viewType } /** * Tracks features individually enabled for the assembler. No features are enabled by default. @@ -1119,8 +1121,7 @@ class StatePlayerRecyclerViewAssembler private constructor( gcsEntityId: String, supportsConceptCards: Boolean ): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> SubmittedAnswerListItemBinding.inflate( @@ -1141,8 +1142,7 @@ class StatePlayerRecyclerViewAssembler private constructor( gcsEntityId: String, supportsConceptCards: Boolean ): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewBinder( inflateView = { parent -> SubmittedHtmlAnswerItemBinding.inflate( @@ -1393,7 +1393,9 @@ class StatePlayerRecyclerViewAssembler private constructor( String, @JvmSuppressWildcards InteractionItemFactory>, @BackgroundDispatcher private val backgroundCoroutineDispatcher: CoroutineDispatcher, private val resourceHandler: AppLanguageResourceHandler, - private val translationController: TranslationController + private val translationController: TranslationController, + private val multiAdapterBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, + private val singleAdapterFactory: BindableAdapter.SingleTypeBuilder.Factory ) { /** * Returns a new [Builder] for the specified GCS resource bucket information for loading @@ -1411,7 +1413,9 @@ class StatePlayerRecyclerViewAssembler private constructor( interactionViewModelFactoryMap, backgroundCoroutineDispatcher, resourceHandler, - translationController + translationController, + multiAdapterBuilderFactory, + singleAdapterFactory ) } } diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt index 848440ca3a7..1dfcbff875a 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt @@ -65,7 +65,8 @@ class ProfileChooserFragmentPresenter @Inject constructor( private val context: Context, private val viewModelProvider: ViewModelProvider, private val profileManagementController: ProfileManagementController, - private val oppiaLogger: OppiaLogger + private val oppiaLogger: OppiaLogger, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private lateinit var binding: ProfileChooserFragmentBinding val hasProfileEverBeenAddedValue = ObservableField(true) @@ -148,10 +149,10 @@ class ProfileChooserFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder( - ProfileChooserUiModel::getModelTypeCase - ) + return multiTypeBuilderFactory.create( + ProfileChooserUiModel::getModelTypeCase + ) .registerViewDataBinderWithSameModelType( viewType = ProfileChooserUiModel.ModelTypeCase.PROFILE, inflateDataBinding = ProfileChooserProfileViewBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentPresenter.kt index 71f4b0451b5..3d01e34a33d 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentPresenter.kt @@ -20,12 +20,11 @@ private const val TAG_PROFILE_PICTURE_EDIT_DIALOG = "PROFILE_PICTURE_EDIT_DIALOG @FragmentScope class ProfileProgressFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, - private val fragment: Fragment + private val fragment: Fragment, + private val viewModel: ProfileProgressViewModel, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { - @Inject - lateinit var viewModel: ProfileProgressViewModel - fun handleCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -70,14 +69,13 @@ class ProfileProgressFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is ProfileProgressHeaderViewModel -> ViewType.VIEW_TYPE_HEADER - is RecentlyPlayedStorySummaryViewModel -> ViewType.VIEW_TYPE_RECENTLY_PLAYED_STORY - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is ProfileProgressHeaderViewModel -> ViewType.VIEW_TYPE_HEADER + is RecentlyPlayedStorySummaryViewModel -> ViewType.VIEW_TYPE_RECENTLY_PLAYED_STORY + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_HEADER, inflateDataBinding = ProfileProgressHeaderBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/recyclerview/BindableAdapter.kt b/app/src/main/java/org/oppia/android/app/recyclerview/BindableAdapter.kt index 7aeebf5f67b..018be6cfd91 100644 --- a/app/src/main/java/org/oppia/android/app/recyclerview/BindableAdapter.kt +++ b/app/src/main/java/org/oppia/android/app/recyclerview/BindableAdapter.kt @@ -4,11 +4,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView -import org.oppia.android.app.recyclerview.BindableAdapter.MultiTypeBuilder.Companion.newBuilder -import org.oppia.android.app.recyclerview.BindableAdapter.SingleTypeBuilder.Companion.newBuilder import java.lang.ref.WeakReference +import javax.inject.Inject import kotlin.reflect.KClass /** A function that returns the integer-based type of view that can bind the specified object. */ @@ -99,59 +99,40 @@ class BindableAdapter internal constructor( * The base builder for [BindableAdapter]. This class should not be used directly--use either * [SingleTypeBuilder] or [MultiTypeBuilder] instead. */ - abstract class BaseBuilder { + abstract class BaseBuilder(fragment: Fragment) { /** - * A [WeakReference] to a [LifecycleOwner] for databinding inflation. See [setLifecycleOwner]. + * A [WeakReference] to a [LifecycleOwner] for databinding inflation. * Note that this needs to be a weak reference so that long-held references to the adapter do * not potentially leak lifecycle owners (such as fragments and activities). */ - private var lifecycleOwnerRef: WeakReference? = null + private val lifecycleOwnerRef: WeakReference = WeakReference(fragment) /** - * Sets the [LifecycleOwner] corresponding to this adapter. This will automatically be used as - * the lifecycle owner for all databinding classes created during view inflation. Note that the - * adapter holds a weak reference to the owner to ensure long-lived references to the adapter - * class itself does not result in leaks, however it's up to the caller's responsibility to make - * sure that the adapter itself is not actually used after the lifecycle owner has expired - * (otherwise the app may crash). + * The [LifecycleOwner] bound to this adapter. * - * @return this - */ - fun setLifecycleOwner(lifecycleOwner: LifecycleOwner): BuilderType { - check(lifecycleOwnerRef == null) { - "A lifecycle owner has already been bound to this adapter." - } - lifecycleOwnerRef = WeakReference(lifecycleOwner) - - // This cast is not, strictly speaking, safe, but child classes are expected to inherit from - // the builder & pass their own type in. - @Suppress("UNCHECKED_CAST") return this as BuilderType - } - - /** - * Returns the [LifecycleOwner] bound to this adapter, or null if there isn't one. This method - * will throw if there was a lifecycle owner bound but is now expired. + * Attempting to call this property will throw if the bound [LifecycleOwner] is expired (i.e. + * indicating that it's been cleaned up by the system and is no longer valid). */ - protected fun getLifecycleOwner(): LifecycleOwner? { - // Crash if the lifecycle owner has been cleaned up since it's not valid to use the adapter - // with an old lifecycle owner, and silently ignoring this may result in part of the layout - // not responding to events. - return lifecycleOwnerRef?.let { ref -> - checkNotNull(ref.get()) { + protected val lifecycleOwner: LifecycleOwner + get() { + // Crash if the lifecycle owner has been cleaned up since it's not valid to use the adapter + // with an old lifecycle owner, and silently ignoring this may result in part of the layout + // not responding to events. + return checkNotNull(lifecycleOwnerRef.get()) { "Attempted to inflate data binding with expired lifecycle owner" } } - } } /** * Constructs a new [BindableAdapter] that for a single view type. * - * Instances of [SingleTypeBuilder] should be instantiated using [newBuilder]. + * Instances of this class can be created by injecting [Factory] and calling [Factory.create]. */ class SingleTypeBuilder( - private val dataClassType: KClass - ) : BaseBuilder>() { + private val dataClassType: KClass, + fragment: Fragment + ) : BaseBuilder(fragment) { private lateinit var viewHolderFactory: ViewHolderFactory /** @@ -222,10 +203,14 @@ class BindableAdapter internal constructor( viewGroup, /* attachToRoot= */ false ) - binding.lifecycleOwner = getLifecycleOwner() + object : BindableViewHolder(binding.root) { override fun bind(data: T) { setViewModel(binding, data) + + // Attaching lifecycleOwner before view model initialization can sometimes cause a + // NullPointerException because data might not be attached to the views yet. + binding.lifecycleOwner = lifecycleOwner } } } @@ -242,11 +227,11 @@ class BindableAdapter internal constructor( ) } - companion object { - /** Returns a new [SingleTypeBuilder]. */ - inline fun newBuilder(): SingleTypeBuilder { - return SingleTypeBuilder(T::class) - } + /** Fragment injectable factory to create new [SingleTypeBuilder]. */ + class Factory @Inject constructor(val fragment: Fragment) { + /** Returns a new [SingleTypeBuilder] for the specified Data class type. */ + inline fun create(): SingleTypeBuilder = + SingleTypeBuilder(T::class, fragment) } } @@ -254,19 +239,20 @@ class BindableAdapter internal constructor( * Constructs a new [BindableAdapter] that supports multiple view types. Each type returned by the * computer should have an associated view binder. * - * Instances of [MultiTypeBuilder] should be instantiated using [newBuilder]. + * Instances of this class can be created by injecting [Factory] and calling [Factory.create]. */ class MultiTypeBuilder>( private val dataClassType: KClass, - private val computeViewType: ComputeViewType - ) : BaseBuilder>() { + private val computeViewType: ComputeViewType, + fragment: Fragment + ) : BaseBuilder(fragment) { private var viewHolderFactoryMap: MutableMap> = HashMap() /** * Registers a [View] inflater and bind function for views of the specified view type (with * default value [DEFAULT_VIEW_TYPE] for single-view [RecyclerView]s). Note that the viewType * specified here must be properly returned in the [ComputeViewType] function passed into - * [newBuilder]. + * [Factory.create]. * * The inflateView and bindView functions passed in here must not hold any references to UI * objects except those that own the RecyclerView. @@ -343,10 +329,14 @@ class BindableAdapter internal constructor( viewGroup, /* attachToRoot= */ false ) - binding.lifecycleOwner = getLifecycleOwner() + object : BindableViewHolder(binding.root) { override fun bind(data: T) { setViewModel(binding, transformViewModel(data)) + + // Attaching lifecycleOwner before view model initialization can sometimes cause a + // NullPointerException because data might not be attached to the views yet. + binding.lifecycleOwner = lifecycleOwner } } } @@ -371,16 +361,12 @@ class BindableAdapter internal constructor( ) } - companion object { - /** - * Returns a new [MultiTypeBuilder] with the specified function that returns the enum type of - * view a specific data item corresponds to. - */ - inline fun > newBuilder( + /** Fragment injectable factory to create new [MultiTypeBuilder]. */ + class Factory @Inject constructor(val fragment: Fragment) { + /** Returns a new [MultiTypeBuilder] for the specified data class type. */ + inline fun > create( noinline computeViewType: ComputeViewType - ): MultiTypeBuilder { - return MultiTypeBuilder(T::class, computeViewType) - } + ): MultiTypeBuilder = MultiTypeBuilder(T::class, computeViewType, fragment) } } } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListFragmentPresenter.kt index fdaeec43c3b..41846084168 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListFragmentPresenter.kt @@ -19,7 +19,8 @@ import javax.inject.Inject class ProfileListFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private var isMultipane = false @@ -52,8 +53,7 @@ class ProfileListFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = ProfileListProfileViewBinding::inflate, setViewModel = ::bindProfileView diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index 24795c0024c..7958dfcc8b0 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -52,7 +52,8 @@ class StoryFragmentPresenter @Inject constructor( private val explorationDataController: ExplorationDataController, @DefaultResourceBucketName private val resourceBucketName: String, @TopicHtmlParserEntityType private val entityType: String, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private val routeToExplorationListener = activity as RouteToExplorationListener private val routeToResumeLessonListener = activity as RouteToResumeLessonListener @@ -141,14 +142,13 @@ class StoryFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is StoryHeaderViewModel -> ViewType.VIEW_TYPE_HEADER - is StoryChapterSummaryViewModel -> ViewType.VIEW_TYPE_CHAPTER - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is StoryHeaderViewModel -> ViewType.VIEW_TYPE_HEADER + is StoryChapterSummaryViewModel -> ViewType.VIEW_TYPE_CHAPTER + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_HEADER, inflateDataBinding = StoryHeaderViewBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/testing/BindableAdapterTestFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/BindableAdapterTestFragmentPresenter.kt index a6b255058db..94b8c23f506 100644 --- a/app/src/main/java/org/oppia/android/app/testing/BindableAdapterTestFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/BindableAdapterTestFragmentPresenter.kt @@ -12,6 +12,8 @@ import javax.inject.Inject /** The test-only fragment presenter corresponding to [BindableAdapterTestFragment]. */ class BindableAdapterTestFragmentPresenter @Inject constructor( private val fragment: Fragment, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, private val testBindableAdapterFactory: BindableAdapterFactory, @VisibleForTesting val viewModel: BindableAdapterTestViewModel ) { @@ -22,7 +24,7 @@ class BindableAdapterTestFragmentPresenter @Inject constructor( /* attachToRoot= */ false ) binding.testRecyclerView.apply { - adapter = testBindableAdapterFactory.create(fragment) + adapter = testBindableAdapterFactory.create(singleTypeBuilderFactory, multiTypeBuilderFactory) } binding.let { it.viewModel = viewModel @@ -33,6 +35,9 @@ class BindableAdapterTestFragmentPresenter @Inject constructor( /** Factory for creating new [BindableAdapter]s for the current fragment. */ interface BindableAdapterFactory { - fun create(fragment: Fragment): BindableAdapter + fun create( + singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory, + multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory + ): BindableAdapter } } diff --git a/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivity.kt index 4d491cf0615..0961e6c9343 100644 --- a/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivity.kt @@ -1,18 +1,12 @@ package org.oppia.android.app.testing import android.os.Bundle -import androidx.recyclerview.widget.RecyclerView import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity -import org.oppia.android.app.recyclerview.OnDragEndedListener -import org.oppia.android.app.recyclerview.OnItemDragListener import javax.inject.Inject /** Test Activity used for testing [DragAndDropItemFacilitator] functionality */ -class DragDropTestActivity : - InjectableAppCompatActivity(), - OnItemDragListener, - OnDragEndedListener { +class DragDropTestActivity : InjectableAppCompatActivity() { @Inject lateinit var dragDropTestActivityPresenter: DragDropTestActivityPresenter @@ -22,16 +16,4 @@ class DragDropTestActivity : (activityComponent as ActivityComponentImpl).inject(this) dragDropTestActivityPresenter.handleOnCreate() } - - override fun onItemDragged( - indexFrom: Int, - indexTo: Int, - adapter: RecyclerView.Adapter - ) { - dragDropTestActivityPresenter.onItemDragged(indexFrom, indexTo, adapter) - } - - override fun onDragEnded(adapter: RecyclerView.Adapter) { - dragDropTestActivityPresenter.onDragEnded(adapter) - } } diff --git a/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivityPresenter.kt index 592e74f62bd..1cf39bf624a 100644 --- a/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/DragDropTestActivityPresenter.kt @@ -1,60 +1,27 @@ package org.oppia.android.app.testing -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView import org.oppia.android.R -import org.oppia.android.app.recyclerview.BindableAdapter import javax.inject.Inject /** The presenter for [DragDropTestActivity] */ class DragDropTestActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { - var dataList = mutableListOf("Item 1", "Item 2", "Item 3", "Item 4") - fun handleOnCreate() { activity.setContentView(R.layout.drag_drop_test_activity) - activity.findViewById(R.id.drag_drop_recycler_view).apply { - adapter = createBindableAdapter() - (adapter as BindableAdapter<*>).setDataUnchecked(dataList) + if (getDragDropTestFragment() == null) { + activity.supportFragmentManager.beginTransaction().add( + R.id.drag_drop_test_fragment_placeholder, + DragDropTestFragment.newInstance() + ).commitNow() } } - private fun createBindableAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() - .registerViewBinder( - inflateView = this::inflateTextViewForStringWithoutDataBinding, - bindView = this::bindTextViewForStringWithoutDataBinding - ) - .build() - } - - private fun bindTextViewForStringWithoutDataBinding(textView: TextView, data: String) { - textView.text = data - } - - private fun inflateTextViewForStringWithoutDataBinding(viewGroup: ViewGroup): TextView { - val inflater = LayoutInflater.from(activity) - return inflater.inflate( - R.layout.test_text_view_for_string_no_data_binding, viewGroup, /* attachToRoot= */ false - ) as TextView - } - - fun onItemDragged( - indexFrom: Int, - indexTo: Int, - adapter: RecyclerView.Adapter - ) { - val item = dataList[indexFrom] - dataList.removeAt(indexFrom) - dataList.add(indexTo, item) - adapter.notifyItemMoved(indexFrom, indexTo) - } - - fun onDragEnded(adapter: RecyclerView.Adapter) { - (adapter as BindableAdapter<*>).setDataUnchecked(dataList) + private fun getDragDropTestFragment(): DragDropTestFragment? { + return activity + .supportFragmentManager + .findFragmentById( + R.id.drag_drop_test_fragment_placeholder + ) as? DragDropTestFragment } } diff --git a/app/src/main/java/org/oppia/android/app/testing/DragDropTestFragment.kt b/app/src/main/java/org/oppia/android/app/testing/DragDropTestFragment.kt new file mode 100644 index 00000000000..27a411c36e0 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/DragDropTestFragment.kt @@ -0,0 +1,55 @@ +package org.oppia.android.app.testing + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.recyclerview.OnDragEndedListener +import org.oppia.android.app.recyclerview.OnItemDragListener +import javax.inject.Inject + +/** Test-only fragment used for verifying ``BindableAdapter`` functionality. */ +class DragDropTestFragment : InjectableFragment(), OnItemDragListener, OnDragEndedListener { + + companion object { + /** Returns a new instance of [DragDropTestFragment]. */ + fun newInstance(): DragDropTestFragment { + return DragDropTestFragment() + } + } + + @Inject + lateinit var dragDropTestFragmentPresenter: DragDropTestFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return dragDropTestFragmentPresenter.handleCreateView( + inflater, + container + ) + } + + override fun onDragEnded(adapter: RecyclerView.Adapter) { + dragDropTestFragmentPresenter.onDragEnded(adapter) + } + + override fun onItemDragged( + indexFrom: Int, + indexTo: Int, + adapter: RecyclerView.Adapter + ) { + dragDropTestFragmentPresenter.onItemDragged(indexFrom, indexTo, adapter) + } +} diff --git a/app/src/main/java/org/oppia/android/app/testing/DragDropTestFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/DragDropTestFragmentPresenter.kt new file mode 100644 index 00000000000..56c6dc94410 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/testing/DragDropTestFragmentPresenter.kt @@ -0,0 +1,77 @@ +package org.oppia.android.app.testing + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import org.oppia.android.R +import org.oppia.android.app.recyclerview.BindableAdapter +import org.oppia.android.databinding.DragDropTestFragmentBinding +import javax.inject.Inject + +/** The presenter for [DragDropTestFragment]. */ +class DragDropTestFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory +) { + + private var dataList = mutableListOf("Item 1", "Item 2", "Item 3", "Item 4") + private lateinit var binding: DragDropTestFragmentBinding + + /** This handles OnCreateView() of [DragDropTestFragment]. */ + fun handleCreateView( + inflater: LayoutInflater, + container: ViewGroup? + ): View? { + binding = DragDropTestFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + + binding.dragDropRecyclerView.apply { + adapter = createBindableAdapter() + (adapter as BindableAdapter<*>).setDataUnchecked(dataList) + } + return binding.root + } + + private fun createBindableAdapter(): BindableAdapter { + return singleTypeBuilderFactory.create() + .registerViewBinder( + inflateView = this::inflateTextViewForStringWithoutDataBinding, + bindView = this::bindTextViewForStringWithoutDataBinding + ) + .build() + } + + private fun bindTextViewForStringWithoutDataBinding(textView: TextView, data: String) { + textView.text = data + } + + private fun inflateTextViewForStringWithoutDataBinding(viewGroup: ViewGroup): TextView { + val inflater = LayoutInflater.from(activity) + return inflater.inflate( + R.layout.test_text_view_for_string_no_data_binding, viewGroup, /* attachToRoot= */ false + ) as TextView + } + + /** This handles dragging of items from given position in [DragDropTestFragment]. */ + fun onItemDragged( + indexFrom: Int, + indexTo: Int, + adapter: RecyclerView.Adapter + ) { + val item = dataList[indexFrom] + dataList.removeAt(indexFrom) + dataList.add(indexTo, item) + adapter.notifyItemMoved(indexFrom, indexTo) + } + + /** This receives dragEndedEvent and unchecks data list in [DragDropTestFragment]. */ + fun onDragEnded(adapter: RecyclerView.Adapter) { + (adapter as BindableAdapter<*>).setDataUnchecked(dataList) + } +} diff --git a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestActivity.kt index ba023d37896..3a79dd9d6af 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestActivity.kt @@ -5,6 +5,7 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.utility.ClickableAreasImage +import org.oppia.android.app.utility.OnClickableAreaClickedListener /** Test Activity used for testing [ClickableAreasImage] functionality */ class ImageRegionSelectionTestActivity : InjectableAppCompatActivity() { @@ -21,4 +22,16 @@ class ImageRegionSelectionTestActivity : InjectableAppCompatActivity() { ) .commitNow() } + + /** + * Sets a test [OnClickableAreaClickedListener] that will be called when the image region + * maintained by this test activity is clicked. + */ + fun setMockOnClickableAreaClickedListener(listener: OnClickableAreaClickedListener) { + val fragment = + supportFragmentManager.findFragmentById(R.id.test_fragment_placeholder) + as? ImageRegionSelectionTestFragment + ?: throw AssertionError("Expected fragment to be present.") + fragment.mockOnClickableAreaClickedListener = listener + } } diff --git a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragment.kt b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragment.kt index 1aaaf62bf34..43c0374a19d 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragment.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragment.kt @@ -8,17 +8,21 @@ import android.view.ViewGroup import org.oppia.android.app.fragment.FragmentComponentImpl import org.oppia.android.app.fragment.InjectableFragment import org.oppia.android.app.utility.ClickableAreasImage +import org.oppia.android.app.utility.OnClickableAreaClickedListener +import org.oppia.android.app.utility.RegionClickedEvent import javax.inject.Inject const val IMAGE_REGION_SELECTION_TEST_FRAGMENT_TAG = "image_region_selection_test_fragment" // TODO(#59): Make this fragment only included in relevant tests instead of all prod builds. /** Test Fragment used for testing [ClickableAreasImage] functionality */ -class ImageRegionSelectionTestFragment : InjectableFragment() { +class ImageRegionSelectionTestFragment : InjectableFragment(), OnClickableAreaClickedListener { @Inject lateinit var imageRegionSelectionTestFragmentPresenter: ImageRegionSelectionTestFragmentPresenter + lateinit var mockOnClickableAreaClickedListener: OnClickableAreaClickedListener + override fun onAttach(context: Context) { super.onAttach(context) (fragmentComponent as FragmentComponentImpl).inject(this) @@ -31,4 +35,8 @@ class ImageRegionSelectionTestFragment : InjectableFragment() { ): View? { return imageRegionSelectionTestFragmentPresenter.handleCreateView(inflater, container) } + + override fun onClickableAreaTouched(region: RegionClickedEvent) { + mockOnClickableAreaClickedListener.onClickableAreaTouched(region) + } } diff --git a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt index 058a8334426..2abb40de558 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt @@ -3,24 +3,30 @@ package org.oppia.android.app.testing import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import org.oppia.android.R import org.oppia.android.app.model.ImageWithRegions.LabeledRegion import org.oppia.android.app.model.Point2d import org.oppia.android.app.player.state.ImageRegionSelectionInteractionView +import org.oppia.android.app.utility.OnClickableAreaClickedListener +import org.oppia.android.databinding.ImageRegionSelectionTestFragmentBinding import javax.inject.Inject /** The presenter for [ImageRegionSelectionTestActivity] */ class ImageRegionSelectionTestFragmentPresenter @Inject constructor( - private val activity: AppCompatActivity + private val fragment: Fragment ) { - fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { - val view = inflater.inflate(R.layout.image_region_selection_test_fragment, container, false) + val view = + ImageRegionSelectionTestFragmentBinding.inflate( + inflater, container, /* attachToRoot= */ false + ).root with(view) { val clickableAreas: List = getClickableAreas() - view.findViewById(R.id.clickable_image_view) - .setClickableAreas(clickableAreas) + view.findViewById(R.id.clickable_image_view).apply { + setClickableAreas(clickableAreas) + setOnRegionClicked(fragment as OnClickableAreaClickedListener) + } } return view } diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt index e7b39f49154..bdb858ff1bd 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt @@ -41,7 +41,9 @@ class TopicLessonsFragmentPresenter @Inject constructor( private val explorationDataController: ExplorationDataController, private val explorationCheckpointController: ExplorationCheckpointController, private val topicLessonViewModel: TopicLessonViewModel, - private val accessibilityService: AccessibilityService + private val accessibilityService: AccessibilityService, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { private val routeToResumeLessonListener = activity as RouteToResumeLessonListener @@ -108,14 +110,13 @@ class TopicLessonsFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is StorySummaryViewModel -> ViewType.VIEW_TYPE_STORY_ITEM - is TopicLessonsTitleViewModel -> ViewType.VIEW_TYPE_TITLE_TEXT - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is StorySummaryViewModel -> ViewType.VIEW_TYPE_STORY_ITEM + is TopicLessonsTitleViewModel -> ViewType.VIEW_TYPE_TITLE_TEXT + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewBinder( viewType = ViewType.VIEW_TYPE_TITLE_TEXT, inflateView = { parent -> @@ -227,18 +228,17 @@ class TopicLessonsFragmentPresenter @Inject constructor( } private fun createChapterRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel.chapterPlayState) { - ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES -> ChapterViewType.CHAPTER_LOCKED - ChapterPlayState.COMPLETED -> ChapterViewType.CHAPTER_COMPLETED - ChapterPlayState.IN_PROGRESS_SAVED, ChapterPlayState.IN_PROGRESS_NOT_SAVED, - ChapterPlayState.STARTED_NOT_COMPLETED, ChapterPlayState.COMPLETION_STATUS_UNSPECIFIED - -> ChapterViewType.CHAPTER_IN_PROGRESS - ChapterPlayState.NOT_STARTED -> ChapterViewType.CHAPTER_NOT_STARTED - ChapterPlayState.UNRECOGNIZED -> throw IllegalArgumentException("Play state unknown") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel.chapterPlayState) { + ChapterPlayState.NOT_PLAYABLE_MISSING_PREREQUISITES -> ChapterViewType.CHAPTER_LOCKED + ChapterPlayState.COMPLETED -> ChapterViewType.CHAPTER_COMPLETED + ChapterPlayState.IN_PROGRESS_SAVED, ChapterPlayState.IN_PROGRESS_NOT_SAVED, + ChapterPlayState.STARTED_NOT_COMPLETED, ChapterPlayState.COMPLETION_STATUS_UNSPECIFIED + -> ChapterViewType.CHAPTER_IN_PROGRESS + ChapterPlayState.NOT_STARTED -> ChapterViewType.CHAPTER_NOT_STARTED + ChapterPlayState.UNRECOGNIZED -> throw IllegalArgumentException("Play state unknown") } + } .registerViewDataBinder( viewType = ChapterViewType.CHAPTER_LOCKED, inflateDataBinding = LessonsLockedChapterViewBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt index 3c8390d0afc..043365e680d 100644 --- a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentPresenter.kt @@ -26,7 +26,8 @@ class TopicPracticeFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val oppiaLogger: OppiaLogger, - private val viewModel: TopicPracticeViewModel + private val viewModel: TopicPracticeViewModel, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) : SubtopicSelector { private lateinit var binding: TopicPracticeFragmentBinding private lateinit var linearLayoutManager: LinearLayoutManager @@ -72,15 +73,14 @@ class TopicPracticeFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is TopicPracticeHeaderViewModel -> ViewType.VIEW_TYPE_HEADER - is TopicPracticeSubtopicViewModel -> ViewType.VIEW_TYPE_SKILL - is TopicPracticeFooterViewModel -> ViewType.VIEW_TYPE_FOOTER - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is TopicPracticeHeaderViewModel -> ViewType.VIEW_TYPE_HEADER + is TopicPracticeSubtopicViewModel -> ViewType.VIEW_TYPE_SKILL + is TopicPracticeFooterViewModel -> ViewType.VIEW_TYPE_FOOTER + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_HEADER, inflateDataBinding = TopicPracticeHeaderViewBinding::inflate, diff --git a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt index 2d8e37227c6..7deec3a8613 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentPresenter.kt @@ -21,7 +21,8 @@ import javax.inject.Inject class TopicRevisionFragmentPresenter @Inject constructor( activity: AppCompatActivity, private val fragment: Fragment, - private val viewModel: TopicRevisionViewModel + private val viewModel: TopicRevisionViewModel, + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) : RevisionSubtopicSelector { private lateinit var binding: TopicRevisionFragmentBinding private var internalProfileId: Int = -1 @@ -63,8 +64,7 @@ class TopicRevisionFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() + return singleTypeBuilderFactory.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = TopicRevisionSummaryViewBinding::inflate, setViewModel = TopicRevisionSummaryViewBinding::setViewModel diff --git a/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt b/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt index 30ba360afa7..69d599fd2de 100644 --- a/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt +++ b/app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt @@ -12,19 +12,19 @@ import org.oppia.android.R import org.oppia.android.app.model.ImageWithRegions import org.oppia.android.app.player.state.ImageRegionSelectionInteractionView import org.oppia.android.app.shim.ViewBindingShim -import org.oppia.android.domain.oppialogger.OppiaLogger -import javax.inject.Inject import kotlin.math.roundToInt -/** - * Helper class to handle clicks on an image along with highlighting the selected region . - */ +/** Helper class to handle clicks on an image along with highlighting the selected region. */ class ClickableAreasImage( private val imageView: ImageRegionSelectionInteractionView, private val parentView: FrameLayout, private val listener: OnClickableAreaClickedListener, - private val bindingInterface: ViewBindingShim + bindingInterface: ViewBindingShim, + private val isAccessibilityEnabled: Boolean, + private val clickableAreas: List ) { + private val defaultRegionView by lazy { bindingInterface.getDefaultRegion(parentView) } + init { imageView.setOnTouchListener { view, motionEvent -> if (motionEvent.action == MotionEvent.ACTION_DOWN) { @@ -34,29 +34,25 @@ class ClickableAreasImage( } } - @Inject - lateinit var oppiaLogger: OppiaLogger - /** * Called when an image is clicked. * - * @param view the original view on which the tap/click occurs. * @param x the relative x coordinate according to image * @param y the relative y coordinate according to image */ private fun onPhotoTap(x: Float, y: Float) { - // Show default region for non-accessibility cases and this will be only called when user taps on unspecified region. - if (!imageView.isAccessibilityEnabled()) { + // Show default region for non-accessibility cases and this will be only called when user taps + // on unspecified region. + if (!isAccessibilityEnabled) { resetRegionSelectionViews() - val defaultRegion = bindingInterface.getDefaultRegion(parentView) - defaultRegion.setBackgroundResource(R.drawable.selected_region_background) - defaultRegion.x = x - defaultRegion.y = y + defaultRegionView.setBackgroundResource(R.drawable.selected_region_background) + defaultRegionView.x = x + defaultRegionView.y = y listener.onClickableAreaTouched(DefaultRegionClickedEvent()) } } - /** Function to remove the background from the views.*/ + /** Function to remove the background from the views. */ private fun resetRegionSelectionViews() { parentView.forEachIndexed { index: Int, childView: View -> // Remove any previously selected region excluding 0th index(image view) @@ -66,12 +62,12 @@ class ClickableAreasImage( } } - /** Get X co-ordinate scaled according to image.*/ + /** Get X co-ordinate scaled according to image. */ private fun getXCoordinate(x: Float): Float { return x * getImageViewContentWidth() } - /** Get Y co-ordinate scaled according to image.*/ + /** Get Y co-ordinate scaled according to image. */ private fun getYCoordinate(y: Float): Float { return y * getImageViewContentHeight() } @@ -84,76 +80,65 @@ class ClickableAreasImage( return imageView.height - imageView.paddingTop - imageView.paddingBottom } - /** Add selectable regions to [FrameLayout].*/ + /** Add selectable regions to [FrameLayout]. */ fun addRegionViews() { - parentView.let { - if (it.childCount > 2) { - try { - it.removeViews(2, it.childCount - 1) // remove all other views - } catch (e: IndexOutOfBoundsException) { - if (::oppiaLogger.isInitialized) - oppiaLogger.e( - "ClickableAreaImage", - "Throws exception on Index out of bound", - e - ) + // Remove all views other than the default region & selectable image. + parentView.children.filter { + it.id != imageView.id && it.id != defaultRegionView.id + }.forEach(parentView::removeView) + clickableAreas.forEach { clickableArea -> + val imageRect = RectF( + getXCoordinate(clickableArea.region.area.upperLeft.x), + getYCoordinate(clickableArea.region.area.upperLeft.y), + getXCoordinate(clickableArea.region.area.lowerRight.x), + getYCoordinate(clickableArea.region.area.lowerRight.y) + ) + val layoutParams = FrameLayout.LayoutParams( + imageRect.width().roundToInt(), + imageRect.height().roundToInt() + ) + val newView = View(parentView.context) + // ClickableArea coordinates are not laid-out properly in RTL. The image region coordinates + // are from left-to-right with an upper left origin and touch coordinates from Android start + // from the right in RTL mode. Thus, to avoid this situation, force layout direction to LTR in + // all situations. + ViewCompat.setLayoutDirection(parentView, ViewCompat.LAYOUT_DIRECTION_LTR) + newView.layoutParams = layoutParams + newView.x = imageRect.left + newView.y = imageRect.top + newView.isClickable = true + newView.isFocusable = true + newView.isFocusableInTouchMode = true + newView.tag = clickableArea.label + newView.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + showOrHideRegion(newView, clickableArea) } + return@setOnTouchListener true } - imageView.getClickableAreas().forEach { clickableArea -> - val imageRect = RectF( - getXCoordinate(clickableArea.region.area.upperLeft.x), - getYCoordinate(clickableArea.region.area.upperLeft.y), - getXCoordinate(clickableArea.region.area.lowerRight.x), - getYCoordinate(clickableArea.region.area.lowerRight.y) - ) - val layoutParams = FrameLayout.LayoutParams( - imageRect.width().roundToInt(), - imageRect.height().roundToInt() - ) - val newView = View(it.context) - // ClickableArea coordinates are not laid-out properly in RTL. The image region coordinates are - // from left-to-right with an upper left origin and touch coordinates from Android start from the - // right in RTL mode. Thus, to avoid this situation, force layout direction to LTR in all situations. - ViewCompat.setLayoutDirection(it, ViewCompat.LAYOUT_DIRECTION_LTR) - newView.layoutParams = layoutParams - newView.x = imageRect.left - newView.y = imageRect.top - newView.isClickable = true - newView.isFocusable = true - newView.isFocusableInTouchMode = true - newView.tag = clickableArea.label - newView.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - showOrHideRegion(newView, clickableArea) - } - return@setOnTouchListener true + if (isAccessibilityEnabled) { + // Make default region visibility gone when talkback enabled to avoid any accidental touch. + defaultRegionView.isVisible = false + newView.setOnClickListener { + showOrHideRegion(newView, clickableArea) } - if (imageView.isAccessibilityEnabled()) { - // Make default region visibility gone when talkback enabled to avoid any accidental touch. - val defaultRegion = bindingInterface.getDefaultRegion(parentView) - defaultRegion.isVisible = false - newView.setOnClickListener { - showOrHideRegion(newView, clickableArea) - } - } - newView.contentDescription = clickableArea.contentDescription - it.addView(newView) } + newView.contentDescription = clickableArea.contentDescription + parentView.addView(newView) + } - // Ensure that the children views are properly computed. The specific flow below is - // recommended by https://stackoverflow.com/a/42430695/3689782 where it's also explained in - // great detail. The 'post' seems necessary since, from observation, requesting layout & - // invalidation doesn't always work (perhaps since this method can be called during a layout - // step), so posting ensures that the views are eventually computed. It's not obvious why - // Android sometimes doesn't compute the region view dimensions, but it results in the - // interaction being non-interactive (though it's recoverable with back & forward navigation - // or rotation, this isn't likely to be obvious to learners and it's a generally poor user - // experience). - it.post { - it.children.forEach(View::forceLayout) - it.invalidate() - it.requestLayout() - } + // Ensure that the children views are properly computed. The specific flow below is recommended + // by https://stackoverflow.com/a/42430695/3689782 where it's also explained in great detail. + // The 'post' seems necessary since, from observation, requesting layout & invalidation doesn't + // always work (perhaps since this method can be called during a layout step), so posting + // ensures that the views are eventually computed. It's not obvious why Android sometimes + // doesn't compute the region view dimensions, but it results in the interaction being + // non-interactive (though it's recoverable with back & forward navigation or rotation, this + // isn't likely to be obvious to learners and it's a generally poor user experience). + parentView.post { + parentView.children.forEach(View::forceLayout) + parentView.invalidate() + parentView.requestLayout() } } diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt index 6bae04731dc..0a28354c29b 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicListFragmentPresenter.kt @@ -27,7 +27,8 @@ import javax.inject.Inject class WalkthroughTopicListFragmentPresenter @Inject constructor( val activity: AppCompatActivity, private val fragment: Fragment, - private val viewModel: WalkthroughTopicViewModel + private val viewModel: WalkthroughTopicViewModel, + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory ) { private lateinit var binding: WalkthroughTopicListFragmentBinding private val routeToNextPage = activity as WalkthroughFragmentChangeListener @@ -79,14 +80,13 @@ class WalkthroughTopicListFragmentPresenter @Inject constructor( } private fun createRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.MultiTypeBuilder - .newBuilder { viewModel -> - when (viewModel) { - is WalkthroughTopicHeaderViewModel -> ViewType.VIEW_TYPE_HEADER - is WalkthroughTopicSummaryViewModel -> ViewType.VIEW_TYPE_TOPIC - else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") - } + return multiTypeBuilderFactory.create { viewModel -> + when (viewModel) { + is WalkthroughTopicHeaderViewModel -> ViewType.VIEW_TYPE_HEADER + is WalkthroughTopicSummaryViewModel -> ViewType.VIEW_TYPE_TOPIC + else -> throw IllegalArgumentException("Encountered unexpected view model: $viewModel") } + } .registerViewDataBinder( viewType = ViewType.VIEW_TYPE_HEADER, inflateDataBinding = WalkthroughTopicHeaderViewBinding::inflate, diff --git a/app/src/main/res/layout/drag_drop_interaction_item.xml b/app/src/main/res/layout/drag_drop_interaction_item.xml index fe40177f834..53a7074bcac 100644 --- a/app/src/main/res/layout/drag_drop_interaction_item.xml +++ b/app/src/main/res/layout/drag_drop_interaction_item.xml @@ -64,7 +64,7 @@ app:allowMultipleItemsInSamePosition="@{viewModel.getGroupingStatus()}" app:entityId="@{viewModel.entityId}" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:list="@{viewModel.choiceItems}" + app:draggableData="@{viewModel.choiceItems}" app:onDragEnded="@{(adapter) -> viewModel.onDragEnded(adapter)}" app:onItemDrag="@{(indexFrom, indexTo, adapter) -> viewModel.onItemDragged(indexFrom, indexTo, adapter)}" /> diff --git a/app/src/main/res/layout/drag_drop_test_activity.xml b/app/src/main/res/layout/drag_drop_test_activity.xml index 1a2f89f87d8..17928a10471 100644 --- a/app/src/main/res/layout/drag_drop_test_activity.xml +++ b/app/src/main/res/layout/drag_drop_test_activity.xml @@ -1,7 +1,8 @@ - + android:layout_height="match_parent" + tools:context=".app.testing.DragDropTestActivity"> + diff --git a/app/src/main/res/layout/drag_drop_test_fragment.xml b/app/src/main/res/layout/drag_drop_test_fragment.xml new file mode 100644 index 00000000000..67b3a71e3e6 --- /dev/null +++ b/app/src/main/res/layout/drag_drop_test_fragment.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/image_region_selection_test_fragment.xml b/app/src/main/res/layout/image_region_selection_test_fragment.xml index 5ff07314181..01bd0230c13 100644 --- a/app/src/main/res/layout/image_region_selection_test_fragment.xml +++ b/app/src/main/res/layout/image_region_selection_test_fragment.xml @@ -1,21 +1,26 @@ - + xmlns:tools="http://schemas.android.com/tools"> - + tools:context=".app.testing.ImageRegionSelectionTestActivity"> - - + + + + + diff --git a/app/src/main/res/layout/selection_interaction_item.xml b/app/src/main/res/layout/selection_interaction_item.xml index 7d61474d523..6e9757e1310 100644 --- a/app/src/main/res/layout/selection_interaction_item.xml +++ b/app/src/main/res/layout/selection_interaction_item.xml @@ -54,7 +54,7 @@ android:layout_height="wrap_content" android:divider="@android:color/transparent" app:allOptionsItemInputType="@{viewModel.getSelectionItemInputType()}" - app:data="@{viewModel.choiceItems}" + app:selectionData="@{viewModel.choiceItems}" app:entityId="@{viewModel.entityId}" app:writtenTranslationContext="@{viewModel.writtenTranslationContext}" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> diff --git a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt index c12ad0b6c7c..bd7446d6b43 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario.launch @@ -98,7 +97,6 @@ import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.OppiaTestRule import org.oppia.android.testing.TestLogReportingModule -import org.oppia.android.testing.assertThrows import org.oppia.android.testing.junit.InitializeDefaultLocaleRule import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers @@ -166,7 +164,8 @@ class BindableAdapterTest { @Test fun testSingleTypeAdapter_withOneViewType_noData_bindsNoViews() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createSingleViewTypeNoDataBindingBindableAdapter() } + TestModule.testAdapterFactory = + { singleFactory, _ -> createSingleViewTypeNoDataBindingBindableAdapter(singleFactory) } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -181,7 +180,9 @@ class BindableAdapterTest { @Test fun testSingleTypeAdapter_withOneViewType_setItem_automaticallyBindsView() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createSingleViewTypeNoDataBindingBindableAdapter() } + TestModule.testAdapterFactory = { singleTypeFactory, _ -> + createSingleViewTypeNoDataBindingBindableAdapter(singleTypeFactory) + } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -204,7 +205,9 @@ class BindableAdapterTest { @Test fun testSingleTypeAdapter_withOneViewType_nullData_bindsNoViews() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createSingleViewTypeNoDataBindingBindableAdapter() } + TestModule.testAdapterFactory = { singleTypeFactory, _ -> + createSingleViewTypeNoDataBindingBindableAdapter(singleTypeFactory) + } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -218,7 +221,9 @@ class BindableAdapterTest { @Test fun testSingleTypeAdapter_withOneViewType_setMultipleItems_automaticallyBinds() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createSingleViewTypeNoDataBindingBindableAdapter() } + TestModule.testAdapterFactory = { singleTypeFactory, _ -> + createSingleViewTypeNoDataBindingBindableAdapter(singleTypeFactory) + } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -244,7 +249,8 @@ class BindableAdapterTest { @Test fun testMultiTypeAdapter_withTwoViewTypes_setItems_autoBindsCorrectItemsPerTypes() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createMultiViewTypeNoDataBindingBindableAdapter() } + TestModule.testAdapterFactory = + { _, multiTypeFactory -> createMultiViewTypeNoDataBindingBindableAdapter(multiTypeFactory) } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -280,7 +286,9 @@ class BindableAdapterTest { @Test fun testSingleTypeAdapter_withOneDataBoundViewType_setItem_automaticallyBindsView() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createSingleViewTypeWithDataBindingBindableAdapter() } + TestModule.testAdapterFactory = { singleTypeFactory, _ -> + createSingleViewTypeWithDataBindingBindableAdapter(singleTypeFactory) + } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -303,7 +311,10 @@ class BindableAdapterTest { @Test fun testSingleTypeAdapter_withTwoDataBoundViewTypes_setItems_autoBindsCorrectItemsPerTypes() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createSingleViewTypeWithDataBindingBindableAdapter() } + TestModule.testAdapterFactory = + { singleTypeFactory, _ -> + createSingleViewTypeWithDataBindingBindableAdapter(singleTypeFactory) + } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -330,7 +341,8 @@ class BindableAdapterTest { @Test fun testMultiTypeAdapter_partiallyDataBoundViewTypes_setItems_autoBindsCorrectItemsPerTypes() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createMultiViewTypeWithDataBindingBindableAdapter() } + TestModule.testAdapterFactory = + { _, multiTypeFactory -> createMultiViewTypeWithDataBindingBindableAdapter(multiTypeFactory) } launch(BindableAdapterTestActivity::class.java).use { scenario -> scenario.onActivity { activity -> @@ -363,68 +375,15 @@ class BindableAdapterTest { } } - @Test - fun testSingleTypeAdapter_setLifecycleOwnerTwice_throwsException() { - val testFragment = Fragment() - - val exception = assertThrows(IllegalStateException::class) { - SingleTypeBuilder - .newBuilder() - .setLifecycleOwner(testFragment) - .setLifecycleOwner(testFragment) - .build() - } - assertThat(exception).hasMessageThat().contains("lifecycle owner has already been bound") - } - - @Test - fun testMultiTypeAdapter_setLifecycleOwnerTwice_throwsException() { - val testFragment = Fragment() - - val exception = assertThrows(IllegalStateException::class) { - MultiTypeBuilder - .newBuilder(ViewModelType.Companion::deriveTypeFrom) - .setLifecycleOwner(testFragment) - .setLifecycleOwner(testFragment) - .build() - } - assertThat(exception).hasMessageThat().contains("lifecycle owner has already been bound") - } - - @Test - fun testSingleTypeAdapter_withLiveData_noLifecycleOwner_doesNotRebindLiveDataValues() { - // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createSingleViewTypeWithDataBindingAndLiveDataAdapter() } - - launch(BindableAdapterTestActivity::class.java).use { scenario -> - val itemLiveData = MutableLiveData("initial") - scenario.onActivity { activity -> - val liveData = getRecyclerViewListLiveData(activity) - liveData.value = listOf(LiveDataModel(itemLiveData)) - } - testCoroutineDispatchers.runCurrent() - - itemLiveData.postValue("new value") - testCoroutineDispatchers.runCurrent() - - // Verify that the bound data did not change despite the underlying live data changing. - onView(atPosition(recyclerViewId = R.id.test_recycler_view, position = 0)).check( - matches( - withText("initial") - ) - ) - } - } - @Test fun testSingleTypeAdapter_withLiveData_withLifecycleOwner_rebindsLiveDataValues() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { fragment -> - createSingleViewTypeWithDataBindingAndLiveDataAdapter(lifecycleOwner = fragment) + TestModule.testAdapterFactory = { singleTypeFactory, _ -> + createSingleViewTypeWithDataBindingAndLiveDataAdapter(singleTypeFactory) } launch(BindableAdapterTestActivity::class.java).use { scenario -> - val itemLiveData = MutableLiveData("initial") + val itemLiveData = MutableLiveData("initial") scenario.onActivity { activity -> val liveData = getRecyclerViewListLiveData(activity) liveData.value = listOf(LiveDataModel(itemLiveData)) @@ -444,40 +403,15 @@ class BindableAdapterTest { } } - @Test - fun testMultiTypeAdapter_withLiveData_noLifecycleOwner_doesNotRebindLiveDataValues() { - // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { createMultiViewTypeWithDataBindingBindableAdapter() } - - launch(BindableAdapterTestActivity::class.java).use { scenario -> - val itemLiveData = MutableLiveData("initial") - scenario.onActivity { activity -> - val liveData = getRecyclerViewListLiveData(activity) - liveData.value = listOf(LiveDataModel(itemLiveData)) - } - testCoroutineDispatchers.runCurrent() - - itemLiveData.postValue("new value") - testCoroutineDispatchers.runCurrent() - - // Verify that the bound data did not change despite the underlying live data changing. - onView(atPosition(recyclerViewId = R.id.test_recycler_view, position = 0)).check( - matches( - withText("initial") - ) - ) - } - } - @Test fun testMultiTypeAdapter_withLiveData_withLifecycleOwner_rebindsLiveDataValues() { // Set up the adapter to be used for this test. - TestModule.testAdapterFactory = { fragment -> - createMultiViewTypeWithDataBindingBindableAdapter(lifecycleOwner = fragment) + TestModule.testAdapterFactory = { _, multiTypeFactory -> + createMultiViewTypeWithDataBindingBindableAdapter(multiTypeFactory) } launch(BindableAdapterTestActivity::class.java).use { scenario -> - val itemLiveData = MutableLiveData("initial") + val itemLiveData = MutableLiveData("initial") scenario.onActivity { activity -> val liveData = getRecyclerViewListLiveData(activity) liveData.value = listOf(LiveDataModel(itemLiveData)) @@ -501,21 +435,22 @@ class BindableAdapterTest { ApplicationProvider.getApplicationContext().inject(this) } - private fun createSingleViewTypeNoDataBindingBindableAdapter(): - BindableAdapter { - return SingleTypeBuilder - .newBuilder() - .registerViewBinder( - inflateView = this::inflateTextViewForStringWithoutDataBinding, - bindView = this::bindTextViewForStringWithoutDataBinding - ) - .build() - } + private fun createSingleViewTypeNoDataBindingBindableAdapter( + singleTypeBuilderFactory: SingleTypeBuilder.Factory + ): BindableAdapter { + return singleTypeBuilderFactory.create() + .registerViewBinder( + inflateView = this::inflateTextViewForStringWithoutDataBinding, + bindView = this::bindTextViewForStringWithoutDataBinding + ) + .build() + } - private fun createSingleViewTypeWithDataBindingBindableAdapter(): + private fun createSingleViewTypeWithDataBindingBindableAdapter( + singleTypeBuilder: SingleTypeBuilder.Factory + ): BindableAdapter { - return SingleTypeBuilder - .newBuilder() + return singleTypeBuilder.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = TestTextViewForStringWithDataBindingBinding::inflate, setViewModel = TestTextViewForStringWithDataBindingBinding::setViewModel @@ -523,23 +458,10 @@ class BindableAdapterTest { .build() } - private fun createSingleViewTypeWithDataBindingAndLiveDataAdapter(): - BindableAdapter { - return SingleTypeBuilder - .newBuilder() - .registerViewDataBinderWithSameModelType( - inflateDataBinding = TestTextViewForLiveDataWithDataBindingBinding::inflate, - setViewModel = TestTextViewForLiveDataWithDataBindingBinding::setViewModel - ) - .build() - } - private fun createSingleViewTypeWithDataBindingAndLiveDataAdapter( - lifecycleOwner: Fragment + singleTypeBuilder: SingleTypeBuilder.Factory ): BindableAdapter { - return SingleTypeBuilder - .newBuilder() - .setLifecycleOwner(lifecycleOwner) + return singleTypeBuilder.create() .registerViewDataBinderWithSameModelType( inflateDataBinding = TestTextViewForLiveDataWithDataBindingBinding::inflate, setViewModel = TestTextViewForLiveDataWithDataBindingBinding::setViewModel @@ -547,10 +469,16 @@ class BindableAdapterTest { .build() } - private fun createMultiViewTypeNoDataBindingBindableAdapter(): + private fun createSingleAdapterWithoutView(singleTypeBuilder: SingleTypeBuilder.Factory): BindableAdapter { - return MultiTypeBuilder - .newBuilder(ViewModelType.Companion::deriveTypeFrom) + return singleTypeBuilder.create().build() + } + + private fun createMultiViewTypeNoDataBindingBindableAdapter( + multiTypeBuilderFactory: MultiTypeBuilder.Factory + ): + BindableAdapter { + return multiTypeBuilderFactory.create(ViewModelType.Companion::deriveTypeFrom) .registerViewBinder( viewType = ViewModelType.STRING, inflateView = this::inflateTextViewForStringWithoutDataBinding, @@ -564,34 +492,10 @@ class BindableAdapterTest { .build() } - private fun createMultiViewTypeWithDataBindingBindableAdapter(): - BindableAdapter { - return MultiTypeBuilder - .newBuilder(ViewModelType.Companion::deriveTypeFrom) - .registerViewDataBinderWithSameModelType( - viewType = ViewModelType.STRING, - inflateDataBinding = TestTextViewForStringWithDataBindingBinding::inflate, - setViewModel = TestTextViewForStringWithDataBindingBinding::setViewModel - ) - .registerViewDataBinderWithSameModelType( - viewType = ViewModelType.INT, - inflateDataBinding = TestTextViewForIntWithDataBindingBinding::inflate, - setViewModel = TestTextViewForIntWithDataBindingBinding::setViewModel - ) - .registerViewDataBinderWithSameModelType( - viewType = ViewModelType.LIVE_DATA, - inflateDataBinding = TestTextViewForLiveDataWithDataBindingBinding::inflate, - setViewModel = TestTextViewForLiveDataWithDataBindingBinding::setViewModel - ) - .build() - } - private fun createMultiViewTypeWithDataBindingBindableAdapter( - lifecycleOwner: Fragment + multiTypeBuilderFactory: MultiTypeBuilder.Factory ): BindableAdapter { - return MultiTypeBuilder - .newBuilder(ViewModelType.Companion::deriveTypeFrom) - .setLifecycleOwner(lifecycleOwner) + return multiTypeBuilderFactory.create(ViewModelType.Companion::deriveTypeFrom) .registerViewDataBinderWithSameModelType( viewType = ViewModelType.STRING, inflateDataBinding = TestTextViewForStringWithDataBindingBinding::inflate, @@ -682,7 +586,10 @@ class BindableAdapterTest { class TestModule { companion object { // TODO(#1720): Move this to a test-level binding to avoid the need for static state. - var testAdapterFactory: ((Fragment) -> BindableAdapter)? = null + var testAdapterFactory: ( + (SingleTypeBuilder.Factory, MultiTypeBuilder.Factory) -> + BindableAdapter + )? = null } @Provides @@ -691,8 +598,11 @@ class BindableAdapterTest { "The test adapter factory hasn't been initialized in the test" } return object : BindableAdapterTestFragmentPresenter.BindableAdapterFactory { - override fun create(fragment: Fragment): BindableAdapter { - return createFunction(fragment) + override fun create( + singleTypeBuilder: SingleTypeBuilder.Factory, + multiTypeBuilderFactory: MultiTypeBuilder.Factory + ): BindableAdapter { + return createFunction(singleTypeBuilder, multiTypeBuilderFactory) } } } diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt index 4f629fa702b..682a8c443bf 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt @@ -192,15 +192,18 @@ class DragDropTestActivityTest { } private fun attachDragDropToActivity(activity: DragDropTestActivity) { - val recyclerView: RecyclerView = activity.findViewById(R.id.drag_drop_recycler_view) - val itemTouchHelper = ItemTouchHelper(createDragCallback(activity)) + val dragDragTestFragment: DragDropTestFragment = activity.supportFragmentManager + .findFragmentById(R.id.drag_drop_test_fragment_placeholder) as DragDropTestFragment + val recyclerView: RecyclerView? = + dragDragTestFragment.view?.findViewById(R.id.drag_drop_recycler_view) + val itemTouchHelper = ItemTouchHelper(createDragCallback(fragment = dragDragTestFragment)) itemTouchHelper.attachToRecyclerView(recyclerView) } - private fun createDragCallback(activity: DragDropTestActivity): ItemTouchHelper.Callback { + private fun createDragCallback(fragment: DragDropTestFragment): ItemTouchHelper.Callback { return DragAndDropItemFacilitator( - activity as OnItemDragListener, - activity as OnDragEndedListener + fragment as OnItemDragListener, + fragment as OnDragEndedListener ) } diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt index a29f03d5667..246a344972b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt @@ -27,7 +27,7 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnit import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponent import org.oppia.android.app.activity.ActivityComponentFactory @@ -39,7 +39,6 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule -import org.oppia.android.app.player.state.ImageRegionSelectionInteractionView import org.oppia.android.app.player.state.StateFragment import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule @@ -82,6 +81,7 @@ import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.OppiaTestRule import org.oppia.android.testing.RunOn +import org.oppia.android.testing.TestImageLoaderModule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.TestPlatform import org.oppia.android.testing.junit.InitializeDefaultLocaleRule @@ -101,8 +101,8 @@ import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule -import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.parser.image.TestGlideImageLoader import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -116,39 +116,29 @@ import javax.inject.Singleton qualifiers = "port-xxhdpi" ) class ImageRegionSelectionInteractionViewTest { - @get:Rule - val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule val oppiaTestRule = OppiaTestRule() + @get:Rule val mockitoRule = MockitoJUnit.rule() - @Inject - lateinit var context: Context + @Mock lateinit var onClickableAreaClickedListener: OnClickableAreaClickedListener + @Captor lateinit var regionClickedEvent: ArgumentCaptor - @Mock - lateinit var onClickableAreaClickedListener: OnClickableAreaClickedListener - - @Captor - lateinit var regionClickedEvent: ArgumentCaptor - - @get:Rule - val oppiaTestRule = OppiaTestRule() + @Inject lateinit var context: Context + @Inject lateinit var imageLoader: TestGlideImageLoader @Before fun setUp() { - setUpTestApplicationComponent() - MockitoAnnotations.initMocks(this) - } - - private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) + imageLoader.arrangeBitmap("test_image_url.drawable", R.drawable.testing_fraction) } @Test // TODO(#1611): Fix ImageRegionSelectionInteractionViewTest @RunOn(TestPlatform.ESPRESSO) fun testImageRegionSelectionInteractionView_clickRegion3_region3Clicked() { - launch(ImageRegionSelectionTestActivity::class.java).use { - it.onActivity { - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.3f, pointY = 0.3f) @@ -171,10 +161,9 @@ class ImageRegionSelectionInteractionViewTest { // TODO(#1611): Fix ImageRegionSelectionInteractionViewTest @RunOn(TestPlatform.ESPRESSO) fun testImageRegionSelectionInteractionView_clickRegion3_clickRegion2_region2Clicked() { - launch(ImageRegionSelectionTestActivity::class.java).use { - it.onActivity { - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.3f, pointY = 0.3f) @@ -213,10 +202,9 @@ class ImageRegionSelectionInteractionViewTest { // TODO(#1611): Fix ImageRegionSelectionInteractionViewTest @RunOn(TestPlatform.ESPRESSO) fun testImageRegionSelectionInteractionView_clickOnDefaultRegion_defaultRegionClicked() { - launch(ImageRegionSelectionTestActivity::class.java).use { - it.onActivity { - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.0f, pointY = 0.0f) @@ -235,10 +223,9 @@ class ImageRegionSelectionInteractionViewTest { @Test @RunOn(TestPlatform.ESPRESSO) fun testView_withTalkbackEnabled_clickRegion3_clickRegion2_region2Clicked() { - launch(ImageRegionSelectionTestActivity::class.java).use { - it.onActivity { - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.3f, pointY = 0.3f) @@ -276,10 +263,9 @@ class ImageRegionSelectionInteractionViewTest { @Test @RunOn(TestPlatform.ESPRESSO) fun testImageRegionSelectionInteractionView_withTalkbackEnabled_clickRegion3_region3Clicked() { - launch(ImageRegionSelectionTestActivity::class.java).use { - it.onActivity { - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.3f, pointY = 0.3f) @@ -306,9 +292,8 @@ class ImageRegionSelectionInteractionViewTest { @Ignore("Move to Robolectric") fun testView_withTalkbackEnabled_clickOnDefaultRegion_defaultRegionNotClicked() { launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> - scenario.onActivity { - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + scenario.onActivity { activity -> + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.0f, pointY = 0.0f) @@ -325,11 +310,10 @@ class ImageRegionSelectionInteractionViewTest { // TODO(#1611): Fix ImageRegionSelectionInteractionViewTest @RunOn(TestPlatform.ESPRESSO) fun testImageRegionSelectionInteractionView_rtl_clickRegion3_region3Clicked() { - launch(ImageRegionSelectionTestActivity::class.java).use { - it.onActivity { - it.window.decorView.layoutDirection = ViewCompat.LAYOUT_DIRECTION_RTL - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + activity.window.decorView.layoutDirection = ViewCompat.LAYOUT_DIRECTION_RTL + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.3f, pointY = 0.3f) @@ -352,11 +336,10 @@ class ImageRegionSelectionInteractionViewTest { // TODO(#1611): Fix ImageRegionSelectionInteractionViewTest @RunOn(TestPlatform.ESPRESSO) fun testImageRegionSelectionInteractionView_rtl_clickRegion3_clickRegion2_region2Clicked() { - launch(ImageRegionSelectionTestActivity::class.java).use { - it.onActivity { - it.window.decorView.layoutDirection = ViewCompat.LAYOUT_DIRECTION_RTL - it.findViewById(R.id.clickable_image_view) - .setListener(onClickableAreaClickedListener) + launch(ImageRegionSelectionTestActivity::class.java).use { scenario -> + scenario.onActivity { activity -> + activity.window.decorView.layoutDirection = ViewCompat.LAYOUT_DIRECTION_RTL + activity.setMockOnClickableAreaClickedListener(onClickableAreaClickedListener) } onView(withId(R.id.clickable_image_view)).perform( clickPoint(pointX = 0.3f, pointY = 0.3f) @@ -402,7 +385,7 @@ class ImageRegionSelectionInteractionViewTest { ItemSelectionInputModule::class, MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, - GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + GcsResourceModule::class, TestImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 615fd3d789f..f45325ca6d7 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -437,6 +437,8 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/HomeTestAct exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/HtmlParserTestActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragment.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/DragDropTestFragmentPresenter.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/DragDropTestFragment.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ImageViewBindingAdaptersTestActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/LessonThumbnailImageViewTestActivity.kt" diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt index 82acfc150b6..7d1f4cbb7dd 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt @@ -1,7 +1,9 @@ package org.oppia.android.util.parser.image +import android.content.Context import android.graphics.Bitmap import android.graphics.drawable.Drawable +import androidx.annotation.DrawableRes import org.oppia.android.util.parser.math.MathModel import org.oppia.android.util.parser.svg.BlockPictureDrawable import javax.inject.Inject @@ -14,8 +16,10 @@ import javax.inject.Singleton */ @Singleton class TestGlideImageLoader @Inject constructor( - private val glideImageLoader: GlideImageLoader + private val glideImageLoader: GlideImageLoader, + private val context: Context ) : ImageLoader { + private val availableBitmaps = mutableMapOf() private val loadedBitmaps = mutableListOf() private val loadedBlockSvgs = mutableListOf() private val loadedTextSvgs = mutableListOf() @@ -27,7 +31,13 @@ class TestGlideImageLoader @Inject constructor( transformations: List ) { loadedBitmaps += imageUrl - glideImageLoader.loadBitmap(imageUrl, target, transformations) + val filename = imageUrl.substringAfterLast('/') + if (filename in availableBitmaps) { + check(target is ImageViewTarget) { + "Only ImageViewTarget-type loads are supported to be overwritten in TestGlideImageLoader." + } + target.imageView.setImageResource(availableBitmaps.getValue(filename)) + } else glideImageLoader.loadBitmap(imageUrl, target, transformations) } override fun loadBlockSvg( @@ -74,6 +84,20 @@ class TestGlideImageLoader @Inject constructor( glideImageLoader.loadMathDrawable(rawLatex, lineHeight, useInlineRendering, target) } + /** + * Sets a test bitmap to load when [loadbitmap] is called, based on a specified filename. + * + * The image loaded will correspond to [imageDrawableResId] instead of being loaded from the + * requested image URL. + * + * Subsequent calls to this method will override any previous arrangements. Multiple filenames may + * point to the same drawable IDs. Referenced drawables do not actually need to be bitmaps (they + * can be any types of drawable). + */ + fun arrangeBitmap(filename: String, @DrawableRes imageDrawableResId: Int) { + availableBitmaps[filename] = imageDrawableResId + } + /** * Returns the list of image URLs that have been loaded as bitmaps since the start of the * application.