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 acdd4a7d328..7f196cd9dc2 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 @@ -45,7 +45,7 @@ class HomeFragmentPresenter @Inject constructor( private val dateTimeUtil: DateTimeUtil, private val translationController: TranslationController ) { - private val routeToTopicListener = activity as RouteToTopicListener + private val routeToTopicPlayStoryListener = activity as RouteToTopicPlayStoryListener private lateinit var binding: HomeFragmentBinding private var internalProfileId: Int = -1 @@ -149,7 +149,11 @@ class HomeFragmentPresenter @Inject constructor( } fun onTopicSummaryClicked(topicSummary: TopicSummary) { - routeToTopicListener.routeToTopic(internalProfileId, topicSummary.topicId) + routeToTopicPlayStoryListener.routeToTopicPlayStory( + internalProfileId, + topicSummary.topicId, + topicSummary.firstStoryId + ) } private fun logHomeActivityEvent() { 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 55347f05b0e..e7b39f49154 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 @@ -17,7 +17,10 @@ import org.oppia.android.app.model.StorySummary import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.topic.RouteToResumeLessonListener import org.oppia.android.app.topic.RouteToStoryListener -import org.oppia.android.databinding.LessonsChapterViewBinding +import org.oppia.android.databinding.LessonsCompletedChapterViewBinding +import org.oppia.android.databinding.LessonsInProgressChapterViewBinding +import org.oppia.android.databinding.LessonsLockedChapterViewBinding +import org.oppia.android.databinding.LessonsNotStartedChapterViewBinding import org.oppia.android.databinding.TopicLessonsFragmentBinding import org.oppia.android.databinding.TopicLessonsStorySummaryBinding import org.oppia.android.databinding.TopicLessonsTitleBinding @@ -224,12 +227,50 @@ class TopicLessonsFragmentPresenter @Inject constructor( } private fun createChapterRecyclerViewAdapter(): BindableAdapter { - return BindableAdapter.SingleTypeBuilder - .newBuilder() - .registerViewDataBinderWithSameModelType( - inflateDataBinding = LessonsChapterViewBinding::inflate, - setViewModel = LessonsChapterViewBinding::setViewModel - ).build() + 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") + } + } + .registerViewDataBinder( + viewType = ChapterViewType.CHAPTER_LOCKED, + inflateDataBinding = LessonsLockedChapterViewBinding::inflate, + setViewModel = LessonsLockedChapterViewBinding::setViewModel, + transformViewModel = { it } + ) + .registerViewDataBinder( + viewType = ChapterViewType.CHAPTER_COMPLETED, + inflateDataBinding = LessonsCompletedChapterViewBinding::inflate, + setViewModel = LessonsCompletedChapterViewBinding::setViewModel, + transformViewModel = { it } + ) + .registerViewDataBinder( + viewType = ChapterViewType.CHAPTER_NOT_STARTED, + inflateDataBinding = LessonsNotStartedChapterViewBinding::inflate, + setViewModel = LessonsNotStartedChapterViewBinding::setViewModel, + transformViewModel = { it } + ) + .registerViewDataBinder( + viewType = ChapterViewType.CHAPTER_IN_PROGRESS, + inflateDataBinding = LessonsInProgressChapterViewBinding::inflate, + setViewModel = LessonsInProgressChapterViewBinding::setViewModel, + transformViewModel = { it } + ) + .build() + } + + private enum class ChapterViewType { + CHAPTER_NOT_STARTED, + CHAPTER_COMPLETED, + CHAPTER_LOCKED, + CHAPTER_IN_PROGRESS } fun storySummaryClicked(storySummary: StorySummary) { diff --git a/app/src/main/res/drawable/ic_baseline_lock_24.xml b/app/src/main/res/drawable/ic_baseline_lock_24.xml index f9eac1afffb..461e25fa7a9 100644 --- a/app/src/main/res/drawable/ic_baseline_lock_24.xml +++ b/app/src/main/res/drawable/ic_baseline_lock_24.xml @@ -1,10 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/ic_pending_24dp.xml b/app/src/main/res/drawable/ic_pending_24dp.xml index f48b88b93d2..2ee26ce1dd3 100644 --- a/app/src/main/res/drawable/ic_pending_24dp.xml +++ b/app/src/main/res/drawable/ic_pending_24dp.xml @@ -4,15 +4,15 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 666992bef7a..2d9f26e8490 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,7 +75,7 @@ Topic Topics in Progress %s\%% - %s. + %s Chapter %s: %s Chapter %s with title %s is completed Chapter %s with title %s is in progress diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt index 22160a03643..5ef996bf4ac 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt @@ -915,6 +915,26 @@ class HomeActivityTest { } } + @Test + fun testHomeActivity_firstTestTopic_topicSummary_opensTopicActivityThroughPlayIntent() { + logIntoUserTwice() + launch(createHomeActivityIntent(internalProfileId1)).use { + testCoroutineDispatchers.runCurrent() + scrollToPosition(5) + onView( + atPositionOnView( + R.id.home_recycler_view, + 5, + R.id.topic_name_text_view + ) + ).check(matches(withText(containsString("Fractions")))).perform(click()) + intended(hasComponent(TopicActivity::class.java.name)) + intended(hasExtra(TopicActivity.getProfileIdKey(), internalProfileId1)) + intended(hasExtra(TopicActivity.getTopicIdKey(), FRACTIONS_TOPIC_ID)) + intended(hasExtra(TopicActivity.getStoryIdKey(), FRACTIONS_STORY_ID_0)) + } + } + @Test fun testHomeActivity_firstTestTopic_topicSummary_topicNameIsCorrect() { logIntoUserTwice() diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt index 365674d6a51..1ac2e61339e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt @@ -322,6 +322,148 @@ class TopicLessonsFragmentTest { } } + @Test + fun testLessonsPlayFragment_loadRatiosTopic_chapterNotStartedIsCorrectlyDisplayed() { + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 0, + targetViewId = R.id.not_started_chapter_container + ) + ).check(matches(isDisplayed())) + } + } + + @Test + fun testLessonsPlayFragment_loadRatiosTopic_chapterLockedIsCorrectlyDisplayed() { + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 1, + targetViewId = R.id.locked_chapter_container + ) + ).check(matches(isDisplayed())) + } + } + + @Test + fun testLessonsPlayFragment_loadRatiosTopic_chapterCompletedIsCorrectlyDisplayed() { + storyProgressTestHelper.markCompletedRatiosStory0Exp0( + profileId, + timestampOlderThanOneWeek = false + ) + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 0, + targetViewId = R.id.lessons_completed_chapter_view + ) + ).check(matches(isDisplayed())) + } + } + + @Test + fun testLessonsPlayFragment_loadRatiosTopic_chapterInProgressIsCorrectlyDisplayed() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( + profileId, + timestampOlderThanOneWeek = false + ) + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 0, + targetViewId = R.id.lessons_in_progress_chapter_container + ) + ).check(matches(isDisplayed())) + } + } + + @Test + fun testLessonsPlayFragment_loadRatiosTopic_configChange_chapterLockedIsCorrectlyDisplayed() { + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView(isRoot()).perform(orientationLandscape()) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 1, + targetViewId = R.id.locked_chapter_container + ) + ).check(matches(isDisplayed())) + } + } + + @Test + fun testLessonsPlayFragment_loadRatiosTopic_configChange_chapterCompletedIsCorrectlyDisplayed() { + storyProgressTestHelper.markCompletedRatiosStory0Exp0( + profileId, + timestampOlderThanOneWeek = false + ) + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView(isRoot()).perform(orientationLandscape()) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 0, + targetViewId = R.id.lessons_completed_chapter_view + ) + ).check(matches(isDisplayed())) + } + } + + @Test + fun testLessonsPlayFragment_loadRatiosTopic_configChange_chapterInProgressIsCorrectlyDisplayed() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( + profileId, + timestampOlderThanOneWeek = false + ) + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView(isRoot()).perform(orientationLandscape()) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 0, + targetViewId = R.id.lessons_in_progress_chapter_container + ) + ).check(matches(isDisplayed())) + } + } + + @Test + fun testLessonsPlayFragment_loadRatiosTopic_configChange_chapterNotStartedIsCorrectlyDisplayed() { + launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + clickLessonTab() + clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) + onView(isRoot()).perform(orientationLandscape()) + onView( + atPositionOnView( + recyclerViewId = R.id.chapter_recycler_view, + position = 0, + targetViewId = R.id.not_started_chapter_container + ) + ).check(matches(isDisplayed())) + } + } + @Test fun testLessonsPlayFragment_loadRatiosTopic_default_arrowDown() { launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { @@ -342,9 +484,14 @@ class TopicLessonsFragmentTest { @Test fun testLessonsPlayFragment_loadRatiosTopic_clickExpandListIcon_chapterListIsVisible() { - launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { + launch( + createTopicPlayStoryActivityIntent( + internalProfileId, + RATIOS_TOPIC_ID, + RATIOS_STORY_ID_0 + ) + ).use { clickLessonTab() - clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) onView( atPositionOnView( recyclerViewId = R.id.story_summary_recycler_view, @@ -377,7 +524,8 @@ class TopicLessonsFragmentTest { position = 1, targetViewId = R.id.chapter_recycler_view ) - ).check(matches(hasDescendant(withId(R.id.chapter_container)))).perform(click()) + ).check(matches(hasDescendant(withId(R.id.lessons_in_progress_chapter_container)))) + .perform(click()) testCoroutineDispatchers.runCurrent() val expectedParams = ResumeLessonActivityParams.newBuilder().apply { @@ -423,7 +571,8 @@ class TopicLessonsFragmentTest { position = 1, targetViewId = R.id.chapter_recycler_view ) - ).check(matches(hasDescendant(withId(R.id.chapter_container)))).perform(click()) + ).check(matches(hasDescendant(withId(R.id.lessons_in_progress_chapter_container)))) + .perform(click()) testCoroutineDispatchers.runCurrent() val expectedParams = ExplorationActivityParams.newBuilder().apply { @@ -455,7 +604,8 @@ class TopicLessonsFragmentTest { position = 1, targetViewId = R.id.chapter_recycler_view ) - ).check(matches(hasDescendant(withId(R.id.chapter_container)))).perform(click()) + ).check(matches(hasDescendant(withId(R.id.not_started_chapter_container)))) + onView(withId(R.id.not_started_chapter_container)).perform(click()) testCoroutineDispatchers.runCurrent() val expectedParams = ExplorationActivityParams.newBuilder().apply { @@ -492,7 +642,8 @@ class TopicLessonsFragmentTest { position = 1, targetViewId = R.id.chapter_recycler_view ) - ).check(matches(hasDescendant(withId(R.id.chapter_container)))).perform(click()) + ).check(matches(hasDescendant(withId(R.id.lessons_in_progress_chapter_container)))) + .perform(click()) testCoroutineDispatchers.runCurrent() val expectedParams = ExplorationActivityParams.newBuilder().apply { @@ -529,7 +680,7 @@ class TopicLessonsFragmentTest { position = 1, targetViewId = R.id.chapter_recycler_view ) - ).check(matches(hasDescendant(withId(R.id.chapter_container)))).perform(click()) + ).check(matches(hasDescendant(withId(R.id.lessons_completed_chapter_view)))).perform(click()) testCoroutineDispatchers.runCurrent() val expectedParams = ExplorationActivityParams.newBuilder().apply { @@ -654,51 +805,6 @@ class TopicLessonsFragmentTest { } } - @Test - fun testLessonPlayFrag_loadRatiosTopic_chapterCompleted_completedIconIsDisplayed() { - storyProgressTestHelper.markCompletedRatiosStory0Exp0( - profileId, - timestampOlderThanOneWeek = false - ) - launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { - clickLessonTab() - clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) - scrollToPosition(position = 1) - verifyChapterPlayStateIconIsVisibleAtPosition(itemPosition = 0) - verifyChapterCompletedIconIsDisplayedAtPosition(itemPosition = 0) - } - } - - @Test - fun testLessonPlayFrag_loadRatiosTopic_chapterCompleted_configChange_completedIconIsDisplayed() { - storyProgressTestHelper.markCompletedRatiosStory0Exp0( - profileId, - timestampOlderThanOneWeek = false - ) - launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { - clickLessonTab() - clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) - scrollToPosition(position = 1) - onView(isRoot()).perform(orientationLandscape()) - verifyChapterPlayStateIconIsVisibleAtPosition(itemPosition = 0) - verifyChapterCompletedIconIsDisplayedAtPosition(itemPosition = 0) - } - } - - @Test - fun testLessonPlayFrag_loadRatiosTopic_unsavedPartialProg_chapterPlayStateIconIsNotVisible() { - storyProgressTestHelper.markInProgressNotSavedRatiosStory0Exp0( - profileId, - timestampOlderThanOneWeek = false - ) - launch(createTopicActivityIntent(internalProfileId, RATIOS_TOPIC_ID)).use { - clickLessonTab() - clickStoryItem(position = 1, targetViewId = R.id.chapter_list_drop_down_icon) - scrollToPosition(position = 1) - verifyChapterPlayStateIconIsNotVisibleAtPosition(itemPosition = 0) - } - } - @Test fun testLessonsPlayFragment_loadRecentStory_default_chapterListIsVisible() { launch( diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt index de0b5576787..769251255a4 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt @@ -212,6 +212,7 @@ class TopicListController @Inject constructor( baseMessage = StoryRecord.getDefaultInstance() ) } + val firstStoryId = storyRecords.getOrNull(0)?.storyId TopicSummary.newBuilder().apply { this.topicId = topicId putAllWrittenTranslations(topicRecord.writtenTranslationsMap) @@ -223,6 +224,7 @@ class TopicListController @Inject constructor( } else { TopicPlayAvailability.newBuilder().setAvailableToPlayInFuture(true).build() } + storyRecords.firstOrNull()?.storyId?.let { this.firstStoryId = it } }.build() } else { createTopicSummaryFromJson(topicId, jsonAssetRetriever.loadJsonFromAsset("$topicId.json")!!) @@ -244,6 +246,9 @@ class TopicListController @Inject constructor( .getJSONArray("node_titles") .length() } + val firstStoryId = + if (storyData.length() == 0) "" else storyData.getJSONObject(0).getStringFromObject("id") + val topicPlayAvailability = if (jsonObject.getBoolean("published")) { TopicPlayAvailability.newBuilder().setAvailableToPlayNow(true).build() } else { @@ -261,6 +266,7 @@ class TopicListController @Inject constructor( .setTotalChapterCount(totalChapterCount) .setTopicThumbnail(createTopicThumbnailFromJson(jsonObject)) .setTopicPlayAvailability(topicPlayAvailability) + .setFirstStoryId(firstStoryId) .build() } diff --git a/model/src/main/proto/topic.proto b/model/src/main/proto/topic.proto index 0b88a8f6b19..582b66af34d 100755 --- a/model/src/main/proto/topic.proto +++ b/model/src/main/proto/topic.proto @@ -345,6 +345,9 @@ message TopicSummary { // The old name corresponding to the topic (as a string). reserved 2; + + // The ID of the first story of this topic. + string first_story_id = 9; } // Corresponds to a topic summary that is currently being viewed. @@ -354,6 +357,9 @@ message EphemeralTopicSummary { // The translation context that should be used for this topic summary. WrittenTranslationContext written_translation_context = 2; + + // The ID of the first story of this topic. + string first_story_id = 7; } // Represents the play state of a single chapter.