diff --git a/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt b/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt new file mode 100644 index 00000000000..05f1009d61c --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt @@ -0,0 +1,245 @@ +package org.oppia.android.domain.classroom + +import org.json.JSONObject +import org.oppia.android.app.model.ClassroomSummary +import org.oppia.android.app.model.EphemeralTopicSummary +import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.StoryRecord +import org.oppia.android.app.model.SubtitledHtml +import org.oppia.android.app.model.TopicIdList +import org.oppia.android.app.model.TopicList +import org.oppia.android.app.model.TopicPlayAvailability +import org.oppia.android.app.model.TopicRecord +import org.oppia.android.app.model.TopicSummary +import org.oppia.android.domain.topic.createTopicThumbnailFromJson +import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.domain.util.JsonAssetRetriever +import org.oppia.android.domain.util.getStringFromObject +import org.oppia.android.util.caching.AssetRepository +import org.oppia.android.util.caching.LoadLessonProtosFromAssets +import org.oppia.android.util.data.DataProvider +import org.oppia.android.util.data.DataProviders.Companion.transform +import org.oppia.android.util.locale.OppiaLocale +import javax.inject.Inject +import javax.inject.Singleton + +private const val GET_CLASSROOM_SUMMARY_LIST_PROVIDER_ID = "get_classroom_summary_list_provider_id" + +@Singleton +class ClassroomController @Inject constructor( + private val jsonAssetRetriever: JsonAssetRetriever, + private val assetRepository: AssetRepository, + private val translationController: TranslationController, + @LoadLessonProtosFromAssets private val loadLessonProtosFromAssets: Boolean +) { + + fun getClassroomList(profileId: ProfileId): DataProvider> { + val translationLocaleProvider = + translationController.getWrittenTranslationContentLocale(profileId) + return translationLocaleProvider.transform( + GET_CLASSROOM_SUMMARY_LIST_PROVIDER_ID, + ::createClassroomSummaryList + ) + } + + private fun createClassroomSummaryList(contentLocale: OppiaLocale.ContentLocale): List { + val classroomSummaryList = mutableListOf() + if (false +// loadLessonProtosFromAssets + ) { +// val classroomIdList = +// assetRepository.loadProtoFromLocalAssets( +// assetName = "classroom", +// baseMessage = ClassroomIdList.getDefaultInstance() +// ) + } else { + val classroomIdJsonArray = jsonAssetRetriever + .loadJsonFromAsset("classrooms.json")!! + .getJSONArray("classroom_id_list") + + for (i in 0 until classroomIdJsonArray.length()) { + val classroomSummary = + createClassroomSummary(contentLocale, classroomIdJsonArray.optString(i)!!) + classroomSummaryList.add(classroomSummary) + } + } + return classroomSummaryList + } + + private fun createClassroomSummary( + contentLocale: OppiaLocale.ContentLocale, + classroomId: String + ): ClassroomSummary { + return if ( + false +// loadLessonProtosFromAssets + ) { + TODO() + } else { + createClassroomSummaryFromJson( + contentLocale, + classroomId + ) + } + } + + private fun createClassroomSummaryFromJson( + contentLocale: OppiaLocale.ContentLocale, + classroomId: String + ): ClassroomSummary { + val topicList = createTopicList(contentLocale, classroomId) + var totalLessonCount = 0 + val topicSummaryCount = topicList.topicSummaryCount + for (i in 0 until topicSummaryCount) { + totalLessonCount += topicList.getTopicSummary(i).topicSummary.totalChapterCount + } + val classroomIdAndNameJsonArray = + jsonAssetRetriever.loadJsonFromAsset("classroomIdAndNameMap.json") + val classroomTitle = SubtitledHtml.newBuilder().apply { + contentId = "title" + html = classroomIdAndNameJsonArray?.getStringFromObject(classroomId) + }.build() + return ClassroomSummary.newBuilder() + .setClassroomId(classroomId) + .setClassroomTitle(classroomTitle) + .setClassroomTitle(SubtitledHtml.getDefaultInstance()) + .setTopicList(topicList) + .setTotalLessonCount(5) + .build() + } + + private fun createTopicList( + contentLocale: OppiaLocale.ContentLocale, + classroomId: String + ): TopicList { + return if (loadLessonProtosFromAssets) { + val topicIdList = + assetRepository.loadProtoFromLocalAssets( + assetName = "topics", + baseMessage = TopicIdList.getDefaultInstance() + ) + return TopicList.newBuilder().apply { + // Only include topics currently playable in the topic list. + addAllTopicSummary( + topicIdList.topicIdsList.map { + createEphemeralTopicSummary(it, contentLocale) + }.filter { + it.topicSummary.topicPlayAvailability.availabilityCase == TopicPlayAvailability.AvailabilityCase.AVAILABLE_TO_PLAY_NOW + }.filter { + it.topicSummary.classroomId == classroomId + } + ) + }.build() + } else loadTopicListFromJson(contentLocale, classroomId) + } + + private fun loadTopicListFromJson( + contentLocale: OppiaLocale.ContentLocale, + classroomId: String + ): TopicList { + val topicIdJsonArray = jsonAssetRetriever + .loadJsonFromAsset("topics.json")!! + .getJSONArray("topic_id_list") + val topicListBuilder = TopicList.newBuilder() + for (i in 0 until topicIdJsonArray.length()) { + val ephemeralSummary = + createEphemeralTopicSummary(topicIdJsonArray.optString(i)!!, contentLocale) + val topicPlayAvailability = ephemeralSummary.topicSummary.topicPlayAvailability + val topicClassroomId = ephemeralSummary.topicSummary.classroomId + // Only include topics currently playable in the topic list and part of the classroomId + if (topicPlayAvailability.availabilityCase == TopicPlayAvailability.AvailabilityCase.AVAILABLE_TO_PLAY_NOW && + topicClassroomId == classroomId + ) { + topicListBuilder.addTopicSummary(ephemeralSummary) + } + } + return topicListBuilder.build() + } + + private fun createEphemeralTopicSummary( + topicId: String, + contentLocale: OppiaLocale.ContentLocale + ): EphemeralTopicSummary { + val topicSummary = createTopicSummary(topicId) + return EphemeralTopicSummary.newBuilder().apply { + this.topicSummary = topicSummary + writtenTranslationContext = + translationController.computeWrittenTranslationContext( + topicSummary.writtenTranslationsMap, + contentLocale + ) + }.build() + } + + private fun createTopicSummary(topicId: String): TopicSummary { + return if (loadLessonProtosFromAssets) { + val topicRecord = + assetRepository.loadProtoFromLocalAssets( + assetName = topicId, + baseMessage = TopicRecord.getDefaultInstance() + ) + val storyRecords = topicRecord.canonicalStoryIdsList.map { + assetRepository.loadProtoFromLocalAssets( + assetName = it, + baseMessage = StoryRecord.getDefaultInstance() + ) + } + TopicSummary.newBuilder().apply { + this.topicId = topicId + putAllWrittenTranslations(topicRecord.writtenTranslationsMap) + title = topicRecord.translatableTitle + classroomId = topicRecord.classroomId + classroomTitle = topicRecord.classroomTitle + totalChapterCount = storyRecords.sumOf { it.chaptersList.size } + topicThumbnail = topicRecord.topicThumbnail + topicPlayAvailability = if (topicRecord.isPublished) { + TopicPlayAvailability.newBuilder().setAvailableToPlayNow(true).build() + } else { + TopicPlayAvailability.newBuilder().setAvailableToPlayInFuture(true).build() + } + storyRecords.firstOrNull()?.storyId?.let { this.firstStoryId = it } + }.build() + } else { + createTopicSummaryFromJson(topicId, jsonAssetRetriever.loadJsonFromAsset("$topicId.json")!!) + } + } + + private fun createTopicSummaryFromJson(topicId: String, jsonObject: JSONObject): TopicSummary { + var totalChapterCount = 0 + val storyData = jsonObject.getJSONArray("canonical_story_dicts") + for (i in 0 until storyData.length()) { + totalChapterCount += storyData + .getJSONObject(i) + .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 { + TopicPlayAvailability.newBuilder().setAvailableToPlayInFuture(true).build() + } + val topicTitle = SubtitledHtml.newBuilder().apply { + contentId = "title" + html = jsonObject.getStringFromObject("topic_name") + }.build() + val classroomTitle = SubtitledHtml.newBuilder().apply { + contentId = "title" + html = jsonObject.getStringFromObject("classroom_title") + } + // No written translations are included since none are retrieved from JSON. + return TopicSummary.newBuilder() + .setTopicId(topicId) + .setTitle(topicTitle) + .setClassroomId(jsonObject.getStringFromObject("classroom_id")) + .setClassroomTitle(classroomTitle) + .setVersion(jsonObject.optInt("version")) + .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 b71bdda02dc..5d5a9bab593 100755 --- a/model/src/main/proto/topic.proto +++ b/model/src/main/proto/topic.proto @@ -22,6 +22,12 @@ message Topic { // The topic's title. SubtitledHtml title = 10; + // The ID of the classroom which contains this Topic. + string classroom_id = 12; + + // The title of the classroom this Topic is part of. + SubtitledHtml classroom_title = 13; + // A brief description of the topic. SubtitledHtml description = 11; @@ -204,6 +210,20 @@ message Classroom { int64 last_update_time_ms = 2; } +message ClassroomSummary { + // The Id of the ClassroomSummary. + string classroom_id = 1; + + // The title of the ClassroomSummary. + SubtitledHtml classroom_title = 2; + + // All topics that are part of this ClassroomSummary. + TopicList topic_list = 3; + + // The total number of lessons (by adding all the lessons from each topic) in this ClassroomSummary. + int32 total_lesson_count = 4; +} + // Corresponds to the list of topics that are currently being played and are not fully finished. message OngoingTopicList { // All topics that are currently being played and have not finished. @@ -289,6 +309,12 @@ message PromotedStory { // The title of the next chapter (exploration title) to complete. SubtitledHtml next_chapter_title = 17; + // The ID of the classroom this story is part of. + string classroom_id = 18; + + // The title of the classroom this story is part of. + SubtitledHtml classroom_title = 19; + // The exploration id next chapter to complete. string exploration_id = 6; @@ -331,9 +357,18 @@ message TopicSummary { // The title of the topic. SubtitledHtml title = 8; + // The ID of the classroom which contains this TopicSummary. + string classroom_id = 10; + + // The title of the classroom this TopicSummary is part of. + SubtitledHtml classroom_title = 11; + // The structural version of the topic. int32 version = 3; + // The number of lessons the player has completed in this topic. + int32 completed_chapter_count = 12; + // The total number of lessons associated with this topic. int32 total_chapter_count = 4; @@ -575,6 +610,12 @@ message TopicRecord { // The topic's description. SubtitledHtml translatable_description = 10; + // The ID of the classroom which contains this TopicSummary. + string classroom_id = 11; + + // The title of the classroom this TopicSummary is part of. + SubtitledHtml classroom_title = 12; + // The list of canonical story IDs that can be used to load stories from the local filesystem. repeated string canonical_story_ids = 4;