From 103f5a811ceb4ad267c57a68982d6c78cddc9d3e Mon Sep 17 00:00:00 2001 From: "Mr. 17" Date: Sun, 30 Jun 2024 08:32:57 +0530 Subject: [PATCH] Fix part of #5344, Fix Part of #5422 and Fix Part of #5012: Implement Classroom List Screen with Jetpack Compose (#5437) ## Explanation Fixes part of #5344 Fixes part of #5422 Fixes part of #5012 This PR introduces the Classroom List Screen, which will replace the existing Home Screen. The new screen features a classroom carousel that remains sticky when the screen is scrolled. Various approaches were considered for implementing the sticky header, as detailed in [Decision 3: How to implement the sticky classroom carousel?](https://docs.google.com/document/d/1wGnoDS-8zHS5QSHufvTf9SXKi8N3lodirs51Xnehdvo/edit#heading=h.fv51u13soqp4). Ultimately, it was decided to use the `stickyHeader` component of Jetpack Compose. - **Introduce Jetpack Compose** - Sets the groundwork for migrating the codebase to Jetpack Compose. The Classroom List Screen uses a `ComposeView` to host the composable components. Appropriate tests have been set up to verify the functionality. - **Migrate away from `onActivityResult`** - Deprecates the use of `onActivityResult` and transitions to using `ActivityResultContracts` for handling activity results. - **Initiate Deprecation of Android KitKat** - The selected versions of Jetpack Compose dependencies require the `minSdkVersion` to be atleast 21. This PR initiates the process of deprecating support for Android 19 (KitKat). https://github.com/oppia/oppia-android/assets/84731134/2a2145e5-5bd0-4d80-9741-ff6baf20fd12 ## Screenshots ### Phone Light Mode |Potrait|Landscape| |--|--| |![image](https://github.com/oppia/oppia-android/assets/84731134/367fe033-833a-4c44-b9ae-05a79c872271)|![image](https://github.com/oppia/oppia-android/assets/84731134/771f3681-884b-45d4-91af-880b775a5937)| |![image](https://github.com/oppia/oppia-android/assets/84731134/921a136c-d84c-479f-a7e6-f40aa55e9b5b)|![image](https://github.com/oppia/oppia-android/assets/84731134/8783f5f8-3b09-4899-b286-09f1d9cfae2f)| ### Phone Dark Mode |Potrait|Landscape| |--|--| |![image](https://github.com/oppia/oppia-android/assets/84731134/2fd50b98-cd5e-45bb-aeff-8690886ddf28)|![image](https://github.com/oppia/oppia-android/assets/84731134/64053fd1-0c22-414a-9cbf-b6fcb615fe38)| |![image](https://github.com/oppia/oppia-android/assets/84731134/2d2160ba-c886-48c7-8832-f1b8327e4daa)|![image](https://github.com/oppia/oppia-android/assets/84731134/7c3974a0-0c4a-4de2-a39b-2239f27bdd64)| ### Tablet Light Mode |Potrait|Landscape| |--|--| |![image](https://github.com/oppia/oppia-android/assets/84731134/459907ce-4bdf-44dd-a84b-e9e50baaa53b)|![image](https://github.com/oppia/oppia-android/assets/84731134/fc3076de-35bf-493e-8b88-e74658285f61)| ||![image](https://github.com/oppia/oppia-android/assets/84731134/34d34a58-e173-488b-9ba4-5de28c2d1342)| ### Tablet Dark Mode |Potrait|Landscape| |--|--| |![image](https://github.com/oppia/oppia-android/assets/84731134/8eff1354-9faa-4717-93df-ae643db0131b)|![image](https://github.com/oppia/oppia-android/assets/84731134/1fe8232d-399f-4268-a889-854e9680529c)| ||![image](https://github.com/oppia/oppia-android/assets/84731134/c8b09dfe-00d9-4d1b-a991-dd2c049d4de1)| ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only If your PR includes UI-related changes, then: - Add screenshots for portrait/landscape for both a tablet & phone of the before & after UI changes - For the screenshots above, include both English and pseudo-localized (RTL) screenshots (see [RTL guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines)) - Add a video showing the full UX flow with a screen reader enabled (see [accessibility guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide)) - For PRs introducing new UI elements or color changes, both light and dark mode screenshots must be included - Add a screenshot demonstrating that you ran affected Espresso tests locally & that they're passing --------- Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> --- BUILD.bazel | 2 +- app/BUILD.bazel | 17 +- app/build.gradle | 23 +- app/src/main/AndroidManifest.xml | 8 + .../app/activity/ActivityComponentImpl.kt | 2 + .../app/classroom/ClassroomListActivity.kt | 115 +++ .../ClassroomListActivityPresenter.kt | 59 ++ .../app/classroom/ClassroomListFragment.kt | 44 + .../ClassroomListFragmentPresenter.kt | 336 +++++++ .../app/classroom/ClassroomListViewModel.kt | 359 ++++++++ .../classroom/classroomlist/ClassroomList.kt | 142 +++ .../classroom/promotedlist/PromotedList.kt | 233 +++++ .../topiclist/AllTopicsHeaderText.kt | 41 + .../app/classroom/topiclist/TopicCard.kt | 108 +++ .../app/classroom/welcome/WelcomeText.kt | 53 ++ .../NavigationDrawerFragmentPresenter.kt | 11 +- .../app/fragment/FragmentComponentImpl.kt | 2 + .../ClassroomSummaryClickListener.kt | 9 + .../ClassroomSummaryViewModel.kt | 39 + .../promotedlist/PromotedStoryViewModel.kt | 5 + .../app/mydownloads/MyDownloadsActivity.kt | 15 +- .../options/AudioLanguageActivityPresenter.kt | 3 +- .../android/app/options/OptionsActivity.kt | 47 +- .../android/app/options/OptionsFragment.kt | 6 - .../app/options/ReadingTextSizeActivity.kt | 2 +- .../android/app/profile/AddProfileActivity.kt | 14 +- .../profile/AddProfileActivityPresenter.kt | 36 +- .../profile/PinPasswordActivityPresenter.kt | 29 +- .../ProfileChooserFragmentPresenter.kt | 23 +- .../ProfileProgressActivity.kt | 20 +- .../ProfileProgressActivityPresenter.kt | 20 +- app/src/main/res/drawable/ic_english.xml | 36 + app/src/main/res/drawable/ic_maths.xml | 69 ++ app/src/main/res/drawable/ic_science.xml | 33 + .../res/layout/classroom_list_activity.xml | 40 + .../res/layout/classroom_list_fragment.xml | 20 + app/src/main/res/values-land/dimens.xml | 12 + .../main/res/values-night/color_palette.xml | 4 + .../main/res/values-sw600dp-land/dimens.xml | 12 + .../main/res/values-sw600dp-port/dimens.xml | 12 + app/src/main/res/values/color_defs.xml | 2 + app/src/main/res/values/color_palette.xml | 5 + app/src/main/res/values/component_colors.xml | 7 + app/src/main/res/values/dimens.xml | 37 +- app/src/main/res/values/strings.xml | 3 + .../classroom/ClassroomListActivityTest.kt | 218 +++++ .../classroom/ClassroomListFragmentTest.kt | 848 ++++++++++++++++++ .../mydownloads/MyDownloadsActivityTest.kt | 42 +- .../app/profile/PinPasswordActivityTest.kt | 49 +- .../app/profile/ProfileChooserFragmentTest.kt | 61 +- app/src/test/AndroidManifest.xml | 2 +- build.gradle | 3 +- build_flavors.bzl | 4 +- data/build.gradle | 5 +- .../data/backends/gae/NetworkModuleTest.kt | 18 - domain/build.gradle | 5 +- .../domain/classroom/ClassroomController.kt | 2 +- domain/src/test/AndroidManifest.xml | 2 +- instrumentation/BUILD.bazel | 2 +- model/src/main/proto/screens.proto | 3 + scripts/assets/maven_dependencies.textproto | 355 +++++++- scripts/assets/test_file_exemptions.textproto | 40 + testing/build.gradle | 5 +- testing/src/test/AndroidManifest.xml | 2 +- third_party/maven_install.json | 637 +++++++++++-- third_party/versions.bzl | 10 +- tools/kotlin/BUILD.bazel | 10 +- utility/build.gradle | 4 +- .../util/logging/EventBundleCreator.kt | 1 + 69 files changed, 4218 insertions(+), 225 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragment.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/ClassroomListViewModel.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/classroomlist/ClassroomList.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/promotedlist/PromotedList.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/topiclist/AllTopicsHeaderText.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/topiclist/TopicCard.kt create mode 100644 app/src/main/java/org/oppia/android/app/classroom/welcome/WelcomeText.kt create mode 100644 app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryClickListener.kt create mode 100644 app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryViewModel.kt create mode 100644 app/src/main/res/drawable/ic_english.xml create mode 100644 app/src/main/res/drawable/ic_maths.xml create mode 100644 app/src/main/res/drawable/ic_science.xml create mode 100644 app/src/main/res/layout/classroom_list_activity.xml create mode 100644 app/src/main/res/layout/classroom_list_fragment.xml create mode 100644 app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListActivityTest.kt create mode 100644 app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt diff --git a/BUILD.bazel b/BUILD.bazel index 5eb1f397a91..f0db399e20c 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -124,7 +124,7 @@ package_group( { "flavor": "oppia_kitkat", "main_dex_list": "//:config/kitkat_main_dex_class_list.txt", - "min_sdk_version": 19, + "min_sdk_version": 21, "multidex": "manual_main_dex", "target_sdk_version": 33, }, diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 0ac39dc4ebf..7ddcebe5300 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -602,6 +602,7 @@ android_library( "//model/src/main/proto:thumbnail_java_proto_lite", "//model/src/main/proto:version_java_proto_lite", "//third_party:androidx_annotation_annotation", + "//third_party:androidx_compose_ui_ui", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_core_core", "//third_party:androidx_databinding_databinding-adapters", @@ -763,6 +764,7 @@ kt_android_library( custom_package = "org.oppia.android.app.ui", enable_data_binding = 1, manifest = "src/main/AppAndroidManifest.xml", + plugins = ["//tools/kotlin:jetpack_compose_compiler_plugin"], visibility = ["//visibility:public"], deps = [ ":binding_adapters", @@ -787,6 +789,15 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/survey:gating_controller", "//domain/src/main/java/org/oppia/android/domain/survey:survey_controller", "//model/src/main/proto:arguments_java_proto_lite", + "//third_party:androidx_activity_activity-compose", + "//third_party:androidx_annotation_annotation", + "//third_party:androidx_appcompat_appcompat", + "//third_party:androidx_compose_foundation_foundation", + "//third_party:androidx_compose_foundation_foundation-layout", + "//third_party:androidx_compose_material_material", + "//third_party:androidx_compose_runtime_runtime", + "//third_party:androidx_compose_ui_ui", + "//third_party:androidx_core_core-ktx", "//third_party:androidx_databinding_databinding-adapters", "//third_party:androidx_databinding_databinding-common", "//third_party:androidx_databinding_databinding-runtime", @@ -800,6 +811,7 @@ kt_android_library( "//third_party:com_github_takusemba_spotlight", "//third_party:com_google_android_flexbox_flexbox", "//third_party:javax_annotation_javax_annotation-api_jar", + "//third_party:org_jetbrains_kotlin_kotlin-stdlib-jdk8_jar", "//utility", "//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions", "//utility/src/main/java/org/oppia/android/util/parser/image:image_loader", @@ -898,6 +910,7 @@ TEST_DEPS = [ "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", "//third_party:androidx_annotation_annotation", + "//third_party:androidx_compose_ui_ui-test-junit4", "//third_party:androidx_core_core", "//third_party:androidx_databinding_databinding-adapters", "//third_party:androidx_databinding_databinding-common", @@ -956,7 +969,7 @@ MIGRATED_TESTS = [ filtered_tests = MIGRATED_TESTS, manifest_values = { "applicationId": "org.oppia.android", - "minSdkVersion": "19", + "minSdkVersion": "21", "targetSdkVersion": "30", "versionCode": "0", "versionName": "0.1-alpha", @@ -972,7 +985,7 @@ MIGRATED_TESTS = [ filtered_tests = MIGRATED_TESTS, manifest_values = { "applicationId": "org.oppia.android", - "minSdkVersion": "19", + "minSdkVersion": "21", "targetSdkVersion": "30", "versionCode": "0", "versionName": "0.1-alpha", diff --git a/app/build.gradle b/app/build.gradle index 05fb3d39c25..13fae2de07a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,10 +6,10 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 - buildToolsVersion "29.0.2" + buildToolsVersion "30.0.2" defaultConfig { applicationId "org.oppia.android" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -30,6 +30,15 @@ android { } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 + useIR = true + freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"] + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + kotlinCompilerVersion kotlin_version } buildTypes { release { @@ -147,6 +156,13 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation( 'androidx.appcompat:appcompat:1.0.2', + "androidx.compose.foundation:foundation:$compose_version", + "androidx.compose.foundation:foundation-layout:$compose_version", + "androidx.compose.material:material:$compose_version", + "androidx.compose.runtime:runtime:$compose_version", + "androidx.compose.runtime:runtime-livedata:$compose_version", + "androidx.compose.ui:ui:$compose_version", + "androidx.compose.ui:ui-tooling:$compose_version", 'androidx.constraintlayout:constraintlayout:1.1.3', 'androidx.core:core-ktx:1.0.2', 'androidx.legacy:legacy-support-v4:1.0.0', @@ -164,6 +180,7 @@ dependencies { 'com.github.bumptech.glide:glide:4.11.0', 'com.google.android.flexbox:flexbox:3.0.0', 'com.google.android.material:material:1.3.0', + "com.google.android.material:compose-theme-adapter:$compose_version", 'com.google.dagger:dagger:2.41', 'com.google.firebase:firebase-analytics:17.5.0', 'com.google.firebase:firebase-analytics-ktx:17.5.0', @@ -189,6 +206,7 @@ dependencies { 'org.glassfish.jaxb:jaxb-runtime:2.3.2', ) testImplementation( + "androidx.compose.ui:ui-test-junit4:$compose_version", 'androidx.test:core:1.2.0', 'androidx.test.espresso:espresso-contrib:3.1.0', 'androidx.test.espresso:espresso-core:3.2.0', @@ -207,6 +225,7 @@ dependencies { project(":testing"), ) androidTestImplementation( + "androidx.compose.ui:ui-test-junit4:$compose_version", 'androidx.test:core:1.2.0', 'androidx.test.espresso:espresso-contrib:3.1.0', 'androidx.test.espresso:espresso-core:3.2.0', diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b85115c35a1..235bb4a479f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -332,11 +332,19 @@ + + diff --git a/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt b/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt index 36ee8d8c1fd..60d24efbd8e 100644 --- a/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt +++ b/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt @@ -6,6 +6,7 @@ import dagger.Subcomponent import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity import org.oppia.android.app.administratorcontrols.appversion.AppVersionActivity import org.oppia.android.app.administratorcontrols.learneranalytics.ProfileAndDeviceIdActivity +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.completedstorylist.CompletedStoryListActivity import org.oppia.android.app.devoptions.DeveloperOptionsActivity import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeActivity @@ -218,4 +219,5 @@ interface ActivityComponentImpl : fun inject(walkthroughActivity: WalkthroughActivity) fun inject(surveyActivity: SurveyActivity) fun inject(colorBindingAdaptersTestActivity: ColorBindingAdaptersTestActivity) + fun inject(classroomListActivity: ClassroomListActivity) } diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt new file mode 100644 index 00000000000..4ba06da7d58 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt @@ -0,0 +1,115 @@ +package org.oppia.android.app.classroom + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity +import org.oppia.android.app.activity.route.ActivityRouter +import org.oppia.android.app.drawer.ExitProfileDialogFragment +import org.oppia.android.app.drawer.TAG_SWITCH_PROFILE_DIALOG +import org.oppia.android.app.home.RouteToRecentlyPlayedListener +import org.oppia.android.app.home.RouteToTopicListener +import org.oppia.android.app.home.RouteToTopicPlayStoryListener +import org.oppia.android.app.model.DestinationScreen +import org.oppia.android.app.model.ExitProfileDialogArguments +import org.oppia.android.app.model.HighlightItem +import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.RecentlyPlayedActivityParams +import org.oppia.android.app.model.RecentlyPlayedActivityTitle +import org.oppia.android.app.model.ScreenName.CLASSROOM_LIST_ACTIVITY +import org.oppia.android.app.topic.TopicActivity +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId +import javax.inject.Inject + +/** The activity for displaying [ClassroomListFragment]. */ +class ClassroomListActivity : + InjectableAutoLocalizedAppCompatActivity(), + RouteToTopicListener, + RouteToTopicPlayStoryListener, + RouteToRecentlyPlayedListener { + @Inject + lateinit var classroomListActivityPresenter: ClassroomListActivityPresenter + + @Inject + lateinit var resourceHandler: AppLanguageResourceHandler + + @Inject + lateinit var activityRouter: ActivityRouter + + private var internalProfileId: Int = -1 + + companion object { + /** Returns a new [Intent] to route to [ClassroomListActivity] for a specified [profileId]. */ + fun createClassroomListActivity(context: Context, profileId: ProfileId?): Intent { + return Intent(context, ClassroomListActivity::class.java).apply { + decorateWithScreenName(CLASSROOM_LIST_ACTIVITY) + profileId?.let { decorateWithUserProfileId(profileId) } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + internalProfileId = intent.extractCurrentUserProfileId().internalId + classroomListActivityPresenter.handleOnCreate() + title = resourceHandler.getStringInLocale(R.string.classroom_list_activity_title) + } + + override fun onRestart() { + super.onRestart() + classroomListActivityPresenter.handleOnRestart() + } + + override fun onBackPressed() { + val previousFragment = + supportFragmentManager.findFragmentByTag(TAG_SWITCH_PROFILE_DIALOG) + if (previousFragment != null) { + supportFragmentManager.beginTransaction().remove(previousFragment).commitNow() + } + val exitProfileDialogArguments = + ExitProfileDialogArguments + .newBuilder() + .setHighlightItem(HighlightItem.NONE) + .build() + val dialogFragment = ExitProfileDialogFragment + .newInstance(exitProfileDialogArguments = exitProfileDialogArguments) + dialogFragment.showNow(supportFragmentManager, TAG_SWITCH_PROFILE_DIALOG) + } + + override fun routeToRecentlyPlayed(recentlyPlayedActivityTitle: RecentlyPlayedActivityTitle) { + val recentlyPlayedActivityParams = + RecentlyPlayedActivityParams + .newBuilder() + .setProfileId(ProfileId.newBuilder().setInternalId(internalProfileId).build()) + .setActivityTitle(recentlyPlayedActivityTitle).build() + + activityRouter.routeToScreen( + DestinationScreen + .newBuilder() + .setRecentlyPlayedActivityParams(recentlyPlayedActivityParams) + .build() + ) + } + + override fun routeToTopic(internalProfileId: Int, topicId: String) { + startActivity(TopicActivity.createTopicActivityIntent(this, internalProfileId, topicId)) + } + + override fun routeToTopicPlayStory(internalProfileId: Int, topicId: String, storyId: String) { + startActivity( + TopicActivity.createTopicPlayStoryActivityIntent( + this, + internalProfileId, + topicId, + storyId + ) + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt new file mode 100644 index 00000000000..ea6a0d8df3f --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt @@ -0,0 +1,59 @@ +package org.oppia.android.app.classroom + +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.drawerlayout.widget.DrawerLayout +import org.oppia.android.R +import org.oppia.android.app.drawer.NavigationDrawerFragment +import javax.inject.Inject + +/** Tag for identifying the [ClassroomListFragment] in transactions. */ +private const val TAG_CLASSROOM_LIST_FRAGMENT = "CLASSROOM_LIST_FRAGMENT" + +/** The presenter for [ClassroomListActivity]. */ +class ClassroomListActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { + private var navigationDrawerFragment: NavigationDrawerFragment? = null + + /** + * Handles the creation of the activity. Sets the content view, sets up the navigation drawer, + * and adds the [ClassroomListFragment] if it's not already added. + */ + fun handleOnCreate() { + activity.setContentView(R.layout.classroom_list_activity) + setUpNavigationDrawer() + if (getClassroomListFragment() == null) { + activity.supportFragmentManager.beginTransaction().add( + R.id.classroom_list_fragment_placeholder, + ClassroomListFragment(), + TAG_CLASSROOM_LIST_FRAGMENT + ).commitNow() + } + } + + /** Handles the activity restart. Re-initializes the navigation drawer. */ + fun handleOnRestart() { + setUpNavigationDrawer() + } + + private fun setUpNavigationDrawer() { + val toolbar = activity.findViewById(R.id.classroom_list_activity_toolbar) as Toolbar + activity.setSupportActionBar(toolbar) + activity.supportActionBar!!.setDisplayShowHomeEnabled(true) + navigationDrawerFragment = activity + .supportFragmentManager + .findFragmentById( + R.id.classroom_list_activity_fragment_navigation_drawer + ) as NavigationDrawerFragment + navigationDrawerFragment!!.setUpDrawer( + activity.findViewById(R.id.classroom_list_activity_drawer_layout) as DrawerLayout, + toolbar, R.id.nav_home + ) + } + + private fun getClassroomListFragment(): ClassroomListFragment? { + return activity.supportFragmentManager.findFragmentById( + R.id.classroom_list_fragment_placeholder + ) as ClassroomListFragment? + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragment.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragment.kt new file mode 100644 index 00000000000..172665fd74f --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragment.kt @@ -0,0 +1,44 @@ +package org.oppia.android.app.classroom + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.app.home.classroomlist.ClassroomSummaryClickListener +import org.oppia.android.app.home.topiclist.TopicSummaryClickListener +import org.oppia.android.app.model.ClassroomSummary +import org.oppia.android.app.model.TopicSummary +import javax.inject.Inject + +/** Fragment that displays the classroom list screen. */ +class ClassroomListFragment : + InjectableFragment(), + TopicSummaryClickListener, + ClassroomSummaryClickListener { + @Inject + lateinit var classroomListFragmentPresenter: ClassroomListFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return classroomListFragmentPresenter.handleCreateView(inflater, container) + } + + override fun onTopicSummaryClicked(topicSummary: TopicSummary) { + classroomListFragmentPresenter.onTopicSummaryClicked(topicSummary) + } + + override fun onClassroomSummaryClicked(classroomSummary: ClassroomSummary) { + classroomListFragmentPresenter.onClassroomSummaryClicked(classroomSummary) + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt new file mode 100644 index 00000000000..7bc81fd5bdf --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt @@ -0,0 +1,336 @@ +package org.oppia.android.app.classroom + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.integerResource +import androidx.compose.ui.unit.dp +import androidx.databinding.ObservableList +import androidx.fragment.app.Fragment +import org.oppia.android.R +import org.oppia.android.app.classroom.classroomlist.ClassroomList +import org.oppia.android.app.classroom.promotedlist.PromotedStoryList +import org.oppia.android.app.classroom.topiclist.AllTopicsHeaderText +import org.oppia.android.app.classroom.topiclist.TopicCard +import org.oppia.android.app.classroom.welcome.WelcomeText +import org.oppia.android.app.home.HomeItemViewModel +import org.oppia.android.app.home.RouteToTopicPlayStoryListener +import org.oppia.android.app.home.WelcomeViewModel +import org.oppia.android.app.home.classroomlist.ClassroomSummaryViewModel +import org.oppia.android.app.home.promotedlist.PromotedStoryListViewModel +import org.oppia.android.app.home.topiclist.AllTopicsViewModel +import org.oppia.android.app.home.topiclist.TopicSummaryViewModel +import org.oppia.android.app.model.ClassroomSummary +import org.oppia.android.app.model.LessonThumbnail +import org.oppia.android.app.model.LessonThumbnailGraphic +import org.oppia.android.app.model.TopicSummary +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.datetime.DateTimeUtil +import org.oppia.android.databinding.ClassroomListFragmentBinding +import org.oppia.android.domain.classroom.ClassroomController +import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.locale.OppiaLocale +import org.oppia.android.util.parser.html.StoryHtmlParserEntityType +import org.oppia.android.util.parser.html.TopicHtmlParserEntityType +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId +import javax.inject.Inject + +/** Test tag for the classroom list screen. */ +const val CLASSROOM_LIST_SCREEN_TEST_TAG = "TEST_TAG.classroom_list_screen" + +/** The presenter for [ClassroomListFragment]. */ +class ClassroomListFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val fragment: Fragment, + private val profileManagementController: ProfileManagementController, + private val topicListController: TopicListController, + private val classroomController: ClassroomController, + private val oppiaLogger: OppiaLogger, + @TopicHtmlParserEntityType private val topicEntityType: String, + @StoryHtmlParserEntityType private val storyEntityType: String, + private val resourceHandler: AppLanguageResourceHandler, + private val dateTimeUtil: DateTimeUtil, + private val translationController: TranslationController, + private val machineLocale: OppiaLocale.MachineLocale, +) { + private val routeToTopicPlayStoryListener = activity as RouteToTopicPlayStoryListener + private lateinit var binding: ClassroomListFragmentBinding + private lateinit var classroomListViewModel: ClassroomListViewModel + private var internalProfileId: Int = -1 + private val profileId = activity.intent.extractCurrentUserProfileId() + + /** Creates and returns the view for the [ClassroomListFragment]. */ + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { + binding = ClassroomListFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + + internalProfileId = profileId.internalId + + classroomListViewModel = ClassroomListViewModel( + activity, + fragment, + oppiaLogger, + internalProfileId, + profileManagementController, + topicListController, + classroomController, + topicEntityType, + storyEntityType, + resourceHandler, + dateTimeUtil, + translationController + ) + + classroomListViewModel.homeItemViewModelListLiveData.observe(activity) { + refreshComposeView() + } + + classroomListViewModel.topicList.addOnListChangedCallback( + object : ObservableList.OnListChangedCallback>() { + override fun onChanged(sender: ObservableList) {} + + override fun onItemRangeChanged( + sender: ObservableList, + positionStart: Int, + itemCount: Int + ) {} + + override fun onItemRangeInserted( + sender: ObservableList, + positionStart: Int, + itemCount: Int + ) { + refreshComposeView() + } + + override fun onItemRangeMoved( + sender: ObservableList, + fromPosition: Int, + toPosition: Int, + itemCount: Int + ) {} + + override fun onItemRangeRemoved( + sender: ObservableList, + positionStart: Int, + itemCount: Int + ) {} + } + ) + + return binding.root + } + + /** Routes to the play story view for the first story in the given topic summary. */ + fun onTopicSummaryClicked(topicSummary: TopicSummary) { + routeToTopicPlayStoryListener.routeToTopicPlayStory( + internalProfileId, + topicSummary.topicId, + topicSummary.firstStoryId + ) + } + + /** Triggers the view model to update the topic list. */ + fun onClassroomSummaryClicked(classroomSummary: ClassroomSummary) { + val classroomId = classroomSummary.classroomId + profileManagementController.updateLastSelectedClassroomId(profileId, classroomId) + classroomListViewModel.fetchAndUpdateTopicList(classroomId) + } + + private fun refreshComposeView() { + binding.composeView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MaterialTheme { + ClassroomListScreen() + } + } + } + } + + /** Display a list of classroom-related items grouped by their types. */ + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun ClassroomListScreen() { + val groupedItems = classroomListViewModel.homeItemViewModelListLiveData.value + ?.plus(classroomListViewModel.topicList) + ?.groupBy { it::class } + val topicListSpanCount = integerResource(id = R.integer.home_span_count) + LazyColumn( + modifier = Modifier.testTag(CLASSROOM_LIST_SCREEN_TEST_TAG) + ) { + groupedItems?.forEach { (type, items) -> + when (type) { + WelcomeViewModel::class -> items.forEach { item -> + item { + WelcomeText(welcomeViewModel = item as WelcomeViewModel) + } + } + PromotedStoryListViewModel::class -> items.forEach { item -> + item { + PromotedStoryList( + promotedStoryListViewModel = item as PromotedStoryListViewModel, + machineLocale = machineLocale + ) + } + } + ClassroomSummaryViewModel::class -> stickyHeader { + ClassroomList( + classroomSummaryList = items.map { it as ClassroomSummaryViewModel }, + classroomListViewModel.selectedClassroomId.get() ?: "" + ) + } + AllTopicsViewModel::class -> items.forEach { _ -> + item { + AllTopicsHeaderText() + } + } + TopicSummaryViewModel::class -> { + gridItems( + data = items.map { it as TopicSummaryViewModel }, + columnCount = topicListSpanCount, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + ) { itemData -> + TopicCard(topicSummaryViewModel = itemData) + } + } + } + } + } + } +} + +/** Adds a grid of items to a LazyListScope with specified arrangement and item content. */ +fun LazyListScope.gridItems( + data: List, + columnCount: Int, + modifier: Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + itemContent: @Composable BoxScope.(T) -> Unit, +) { + val size = data.count() + // Calculate the number of rows needed. + val rows = if (size == 0) 0 else (size + columnCount - 1) / columnCount + + // Generate items in the LazyList. + items(rows, key = { it }) { rowIndex -> + // Create a row with the specified horizontal arrangement and padding. + Row( + horizontalArrangement = horizontalArrangement, + modifier = modifier + .background( + colorResource(id = R.color.component_color_classroom_topic_list_background_color) + ) + .padding( + horizontal = dimensionResource(id = R.dimen.classrooms_text_margin_start), + vertical = 10.dp + ) + ) { + // Populate the row with columns. + for (columnIndex in 0 until columnCount) { + val itemIndex = rowIndex * columnCount + columnIndex + if (itemIndex < size) { + Box( + modifier = Modifier.weight(1F, fill = true), + propagateMinConstraints = true + ) { + itemContent(data[itemIndex]) // Provide content for each item. + } + } else { + Spacer(Modifier.weight(1F, fill = true)) // Add spacer if no more items. + } + } + } + + // Add bottom padding if it's the last row. + if (rowIndex == rows - 1) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(id = R.dimen.home_fragment_padding_bottom)) + .background( + colorResource(id = R.color.component_color_classroom_topic_list_background_color) + ) + ) + } + } +} + +/** Retrieves the drawable resource ID for the lesson thumbnail based on its graphic type. */ +fun LessonThumbnail.getDrawableResource(): Int { + return when (thumbnailGraphic) { + LessonThumbnailGraphic.BAKER -> + R.drawable.lesson_thumbnail_graphic_baker + LessonThumbnailGraphic.CHILD_WITH_BOOK -> + R.drawable.lesson_thumbnail_graphic_child_with_book + LessonThumbnailGraphic.CHILD_WITH_CUPCAKES -> + R.drawable.lesson_thumbnail_graphic_child_with_cupcakes + LessonThumbnailGraphic.CHILD_WITH_FRACTIONS_HOMEWORK -> + R.drawable.lesson_thumbnail_graphic_child_with_fractions_homework + LessonThumbnailGraphic.DUCK_AND_CHICKEN -> + R.drawable.lesson_thumbnail_graphic_duck_and_chicken + LessonThumbnailGraphic.PERSON_WITH_PIE_CHART -> + R.drawable.lesson_thumbnail_graphic_person_with_pie_chart + LessonThumbnailGraphic.IDENTIFYING_THE_PARTS_OF_A_FRACTION -> + R.drawable.topic_fractions_01 + LessonThumbnailGraphic.WRITING_FRACTIONS -> + R.drawable.topic_fractions_02 + LessonThumbnailGraphic.EQUIVALENT_FRACTIONS -> + R.drawable.topic_fractions_03 + LessonThumbnailGraphic.MIXED_NUMBERS_AND_IMPROPER_FRACTIONS -> + R.drawable.topic_fractions_04 + LessonThumbnailGraphic.COMPARING_FRACTIONS -> + R.drawable.topic_fractions_05 + LessonThumbnailGraphic.ADDING_AND_SUBTRACTING_FRACTIONS -> + R.drawable.topic_fractions_06 + LessonThumbnailGraphic.MULTIPLYING_FRACTIONS -> + R.drawable.topic_fractions_07 + LessonThumbnailGraphic.DIVIDING_FRACTIONS -> + R.drawable.topic_fractions_08 + LessonThumbnailGraphic.DERIVE_A_RATIO -> + R.drawable.topic_ratios_01 + LessonThumbnailGraphic.WHAT_IS_A_FRACTION -> + R.drawable.topic_fractions_01 + LessonThumbnailGraphic.FRACTION_OF_A_GROUP -> + R.drawable.topic_fractions_02 + LessonThumbnailGraphic.ADDING_FRACTIONS -> + R.drawable.topic_fractions_03 + LessonThumbnailGraphic.MIXED_NUMBERS -> + R.drawable.topic_fractions_04 + LessonThumbnailGraphic.SCIENCE_CLASSROOM -> + R.drawable.ic_science + LessonThumbnailGraphic.MATHS_CLASSROOM -> + R.drawable.ic_maths + LessonThumbnailGraphic.ENGLISH_CLASSROOM -> + R.drawable.ic_english + else -> + R.drawable.topic_fractions_01 + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListViewModel.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListViewModel.kt new file mode 100644 index 00000000000..df1b5cd8f13 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListViewModel.kt @@ -0,0 +1,359 @@ +package org.oppia.android.app.classroom + +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.ObservableArrayList +import androidx.databinding.ObservableField +import androidx.databinding.ObservableList +import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import org.oppia.android.R +import org.oppia.android.app.home.HomeItemViewModel +import org.oppia.android.app.home.WelcomeViewModel +import org.oppia.android.app.home.classroomlist.ClassroomSummaryClickListener +import org.oppia.android.app.home.classroomlist.ClassroomSummaryViewModel +import org.oppia.android.app.home.promotedlist.ComingSoonTopicListViewModel +import org.oppia.android.app.home.promotedlist.ComingSoonTopicsViewModel +import org.oppia.android.app.home.promotedlist.PromotedStoryListViewModel +import org.oppia.android.app.home.promotedlist.PromotedStoryViewModel +import org.oppia.android.app.home.topiclist.AllTopicsViewModel +import org.oppia.android.app.home.topiclist.TopicSummaryClickListener +import org.oppia.android.app.home.topiclist.TopicSummaryViewModel +import org.oppia.android.app.model.ClassroomList +import org.oppia.android.app.model.ComingSoonTopicList +import org.oppia.android.app.model.Profile +import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.PromotedActivityList +import org.oppia.android.app.model.PromotedStoryList +import org.oppia.android.app.model.TopicList +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.datetime.DateTimeUtil +import org.oppia.android.app.viewmodel.ObservableViewModel +import org.oppia.android.domain.classroom.ClassroomController +import org.oppia.android.domain.classroom.TEST_CLASSROOM_ID_0 +import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProvider +import org.oppia.android.util.data.DataProviders.Companion.combineWith +import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import org.oppia.android.util.parser.html.StoryHtmlParserEntityType +import org.oppia.android.util.parser.html.TopicHtmlParserEntityType + +private const val PROFILE_AND_PROMOTED_ACTIVITY_COMBINED_PROVIDER_ID = + "profile+promotedActivityList" +private const val CLASSROOM_LIST_FRAGMENT_COMBINED_PROVIDER_ID = + "profile+promotedActivityList+classroomListProvider" + +/** [ViewModel] for layouts in classroom list fragment. */ +class ClassroomListViewModel( + private val activity: AppCompatActivity, + private val fragment: Fragment, + private val oppiaLogger: OppiaLogger, + private val internalProfileId: Int, + private val profileManagementController: ProfileManagementController, + private val topicListController: TopicListController, + private val classroomController: ClassroomController, + @TopicHtmlParserEntityType private val topicEntityType: String, + @StoryHtmlParserEntityType private val storyEntityType: String, + private val resourceHandler: AppLanguageResourceHandler, + private val dateTimeUtil: DateTimeUtil, + private val translationController: TranslationController +) : ObservableViewModel() { + private val profileId: ProfileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + private val promotedStoryListLimit = activity.resources.getInteger( + R.integer.promoted_story_list_limit + ) + + /** An observable boolean property indicating the visibility state of the progress bar. */ + val isProgressBarVisible = ObservableField(true) + + /** An observable field to store the currently selected classroom ID. */ + val selectedClassroomId = ObservableField("") + + private val profileDataProvider: DataProvider by lazy { + profileManagementController.getProfile(profileId) + } + + private val promotedActivityListSummaryDataProvider: DataProvider by lazy { + topicListController.getPromotedActivityList(profileId) + } + + private val classroomSummaryListDataProvider: DataProvider by lazy { + classroomController.getClassroomList(profileId) + } + + /** + * An observable list containing the topic list of the selected classroom. The topic list updates + * when a new classroom is selected by the user. + */ + val topicList: ObservableList by lazy { + fetchAndUpdateTopicList() // Fetch the topic list of the last selected classroom. + ObservableArrayList() // Initialize the topic list. + } + + private val homeItemViewModelListDataProvider: DataProvider> by lazy { + // This will block until all data providers return initial results (which may be default + // instances). If any of the data providers are pending or failed, the combined result will also + // be pending or failed. + profileDataProvider.combineWith( + promotedActivityListSummaryDataProvider, + PROFILE_AND_PROMOTED_ACTIVITY_COMBINED_PROVIDER_ID + ) { profile, promotedActivityList -> + if (profile.numberOfLogins > 1) { + listOfNotNull( + computeWelcomeViewModel(profile), + computePromotedActivityListViewModel(promotedActivityList) + ) + } else { + listOfNotNull(computeWelcomeViewModel(profile)) + } + }.combineWith( + classroomSummaryListDataProvider, + CLASSROOM_LIST_FRAGMENT_COMBINED_PROVIDER_ID + ) { homeItemViewModelList, classroomSummaryList -> + homeItemViewModelList + computeClassroomItemViewModelList(classroomSummaryList) + } + } + + /** + * [LiveData] of the list of items displayed in the [ClassroomListFragment] RecyclerView. + * The list backing this live data will automatically update if constituent parts of the UI + * change (e.g. if the promoted story list changes). If an error occurs or data providers are + * still pending, the list is empty and so the view shown will be empty. + */ + val homeItemViewModelListLiveData: LiveData> by lazy { + Transformations.map(homeItemViewModelListDataProvider.toLiveData()) { itemListResult -> + return@map when (itemListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ClassroomListFragment", + "No classroom list fragment available -- failed to retrieve fragment data.", + itemListResult.error + ) + listOf() + } + is AsyncResult.Pending -> listOf() + is AsyncResult.Success -> { + isProgressBarVisible.set(false) + itemListResult.value + } + } + } + } + + /** + * Returns a [HomeItemViewModel] corresponding to the welcome message (see [WelcomeViewModel]), or null if + * the specified profile has insufficient information to show the welcome message. + */ + private fun computeWelcomeViewModel(profile: Profile): HomeItemViewModel? { + return if (profile.name.isNotEmpty()) { + WelcomeViewModel(profile.name, resourceHandler, dateTimeUtil) + } else null + } + + /** + * Returns a [HomeItemViewModel] corresponding to the promoted stories(Recommended, Recently-played and + * Last-played stories)[PromotedStoryListViewModel] and Upcoming topics [ComingSoonTopicListViewModel] + * to be displayed for this learner or null if this profile does not have any promoted stories. + * Promoted stories are determined by any recent stories last-played stories or suggested stories started by this profile. + */ + private fun computePromotedActivityListViewModel( + promotedActivityList: PromotedActivityList + ): HomeItemViewModel? { + when (promotedActivityList.recommendationTypeCase) { + PromotedActivityList.RecommendationTypeCase.PROMOTED_STORY_LIST -> { + val storyViewModelList = computePromotedStoryViewModelList( + promotedActivityList.promotedStoryList + ) + return if (storyViewModelList.isNotEmpty()) { + return PromotedStoryListViewModel( + activity, + storyViewModelList, + promotedActivityList, + resourceHandler + ) + } else null + } + PromotedActivityList.RecommendationTypeCase.COMING_SOON_TOPIC_LIST -> { + val comingSoonTopicsList = computeComingSoonTopicViewModelList( + promotedActivityList.comingSoonTopicList + ) + return if (comingSoonTopicsList.isNotEmpty()) { + return ComingSoonTopicListViewModel( + comingSoonTopicsList + ) + } else null + } + else -> return null + } + } + + /** + * Returns a list of [HomeItemViewModel]s corresponding to the the [PromotedStoryListViewModel] displayed + * for this profile (see [PromotedStoryViewModel]), or an empty list if the profile does not have any + * ongoing stories at all. + */ + private fun computePromotedStoryViewModelList( + promotedStoryList: PromotedStoryList + ): List { + with(promotedStoryList) { + val storyList = when { + suggestedStoryList.isNotEmpty() -> { + if (recentlyPlayedStoryList.isNotEmpty() || olderPlayedStoryList.isNotEmpty()) { + recentlyPlayedStoryList + + olderPlayedStoryList + + suggestedStoryList + } else { + suggestedStoryList + } + } + recentlyPlayedStoryList.isNotEmpty() -> { + recentlyPlayedStoryList + } + else -> { + olderPlayedStoryList + } + } + + // Check if at least one story in topic is completed. Prioritize recommended story over + // completed story topic. + val sortedStoryList = storyList.sortedByDescending { !it.isTopicLearned } + return sortedStoryList.take(promotedStoryListLimit) + .mapIndexed { index, promotedStory -> + PromotedStoryViewModel( + activity, + internalProfileId, + sortedStoryList.size, + storyEntityType, + promotedStory, + translationController, + index + ) + } + } + } + + /** + * Returns a list of [HomeItemViewModel]s corresponding to [ComingSoonTopicListViewModel] all the upcoming topics available in future and to be + * displayed for this profile (see [ComingSoonTopicsViewModel]), or an empty list if the profile does not have any + * ongoing stories at all. + */ + private fun computeComingSoonTopicViewModelList( + comingSoonTopicList: ComingSoonTopicList + ): List { + return comingSoonTopicList.upcomingTopicList.map { topicSummary -> + ComingSoonTopicsViewModel( + activity, + topicSummary, + topicEntityType, + comingSoonTopicList, + translationController + ) + } + } + + /** + * Returns a list of [HomeItemViewModel]s corresponding to all the classroom summaries available + * and to be displayed on the [ClassroomListActivity] (see [ClassroomSummaryViewModel]). Returns + * an empty list if there are no classrooms to display to the learner (caused by either + * error or pending data providers). + */ + private fun computeClassroomItemViewModelList( + classroomList: ClassroomList + ): List { + return classroomList.classroomSummaryList.map { ephemeralClassroomSummary -> + ClassroomSummaryViewModel( + fragment as ClassroomSummaryClickListener, + ephemeralClassroomSummary, + translationController + ) + } + } + + /** + * Returns a list of [HomeItemViewModel]s corresponding to all the lesson topics available and to + * be displayed on the [ClassroomListActivity] (see [TopicSummaryViewModel]) along with + * associated topics list header (see [AllTopicsViewModel]). Returns an empty list if there are + * no topics to display to the learner (caused by either error or pending data providers). + */ + private fun computeAllTopicsItemsViewModelList( + topicList: TopicList + ): List { + val allTopicsList = topicList.topicSummaryList.mapIndexed { topicIndex, ephemeralSummary -> + TopicSummaryViewModel( + activity, + ephemeralSummary, + topicEntityType, + fragment as TopicSummaryClickListener, + position = topicIndex, + resourceHandler, + translationController + ) + } + return if (allTopicsList.isNotEmpty()) { + listOf(AllTopicsViewModel) + allTopicsList + } else emptyList() + } + + /** Fetches and updates the topic list based on the provided or last selected classroom ID. */ + fun fetchAndUpdateTopicList(classroomId: String = "") { + if (classroomId.isBlank()) { + // Retrieve the last selected classroom ID if no specific classroom ID is provided. + profileManagementController.retrieveLastSelectedClassroomId(profileId) + .toLiveData().observe(fragment) { lastSelectedClassroomIdResult -> + when (lastSelectedClassroomIdResult) { + is AsyncResult.Success -> { + val lastSelectedClassroomId = lastSelectedClassroomIdResult.value + updateTopicList( + if (lastSelectedClassroomId.isNullOrBlank()) + TEST_CLASSROOM_ID_0 + else + lastSelectedClassroomId + ) + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "ClassroomListFragment", + "Failed to retrieve last selected classroom ID", + lastSelectedClassroomIdResult.error + ) + // Use a default classroom ID in case of failure. + updateTopicList(TEST_CLASSROOM_ID_0) + } + is AsyncResult.Pending -> {} + } + } + } else { + // Fetch the topic list using the provided classroom ID. + updateTopicList(classroomId) + } + } + + private fun updateTopicList(classroomId: String) { + selectedClassroomId.set(classroomId) + classroomController.getTopicList( + profileId, + classroomId + ).toLiveData().observe(fragment) { topicListResult -> + when (topicListResult) { + is AsyncResult.Success -> { + topicList.clear() + computeAllTopicsItemsViewModelList(topicListResult.value).map { itemViewModel -> + topicList.add(itemViewModel) + } + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "ClassroomListFragment", + "Failed to retrieve topic list.", + topicListResult.error + ) + } + is AsyncResult.Pending -> {} + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/classroomlist/ClassroomList.kt b/app/src/main/java/org/oppia/android/app/classroom/classroomlist/ClassroomList.kt new file mode 100644 index 00000000000..5c35b3383b1 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/classroomlist/ClassroomList.kt @@ -0,0 +1,142 @@ +package org.oppia.android.app.classroom.classroomlist + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.oppia.android.R +import org.oppia.android.app.classroom.getDrawableResource +import org.oppia.android.app.home.classroomlist.ClassroomSummaryViewModel + +/** Test tag for the header of the classroom section. */ +const val CLASSROOM_HEADER_TEST_TAG = "TEST_TAG.classroom_header" + +/** Test tag for the classroom list. */ +const val CLASSROOM_LIST_TEST_TAG = "TEST_TAG.classroom_list" + +/** Displays a list of classroom summaries with a header. */ +@Composable +fun ClassroomList( + classroomSummaryList: List, + selectedClassroomId: String +) { + Column( + modifier = Modifier + .background( + color = colorResource(id = R.color.component_color_shared_screen_primary_background_color) + ) + .fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.classrooms_list_activity_section_header), + color = colorResource(id = R.color.component_color_shared_primary_text_color), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = dimensionResource(id = R.dimen.classrooms_list_header_text_size).value.sp, + modifier = Modifier + .testTag(CLASSROOM_HEADER_TEST_TAG) + .padding( + start = dimensionResource(id = R.dimen.classrooms_text_margin_start), + top = dimensionResource(id = R.dimen.classrooms_text_margin_top), + end = dimensionResource(id = R.dimen.classrooms_text_margin_end), + bottom = dimensionResource(id = R.dimen.classrooms_text_margin_bottom), + ), + ) + LazyRow( + modifier = Modifier.testTag(CLASSROOM_LIST_TEST_TAG), + contentPadding = PaddingValues( + start = dimensionResource(id = R.dimen.classrooms_text_margin_start), + end = dimensionResource(id = R.dimen.classrooms_text_margin_end), + ), + ) { + items(classroomSummaryList) { + ClassroomCard(classroomSummaryViewModel = it, selectedClassroomId) + } + } + } +} + +/** Displays a single classroom card with an image and text, handling click events. */ +@Composable +fun ClassroomCard( + classroomSummaryViewModel: ClassroomSummaryViewModel, + selectedClassroomId: String +) { + val screenWidth = LocalConfiguration.current.screenWidthDp + val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT + val isTablet = if (isPortrait) screenWidth > 600 else screenWidth > 840 + val isCardSelected = classroomSummaryViewModel.classroomSummary.classroomId == selectedClassroomId + Card( + modifier = Modifier + .height(dimensionResource(id = R.dimen.classrooms_card_height)) + .width(dimensionResource(id = R.dimen.classrooms_card_width)) + .padding( + start = dimensionResource(R.dimen.promoted_story_card_layout_margin_start), + end = dimensionResource(R.dimen.promoted_story_card_layout_margin_end), + ) + .clickable { + classroomSummaryViewModel.handleClassroomClick() + }, + backgroundColor = if (isCardSelected) { + colorResource(id = R.color.component_color_classroom_card_color) + } else { + colorResource(id = R.color.component_color_shared_screen_primary_background_color) + }, + border = BorderStroke(2.dp, color = colorResource(id = R.color.color_def_oppia_green)), + elevation = dimensionResource(id = R.dimen.classrooms_card_elevation), + ) { + Column( + modifier = Modifier.padding(all = dimensionResource(id = R.dimen.classrooms_card_padding)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (isPortrait || isTablet) { // Hides the classroom icon for landscape phone layouts. + Image( + painter = painterResource( + id = classroomSummaryViewModel + .classroomSummary + .classroomThumbnail + .getDrawableResource() + ), + contentDescription = classroomSummaryViewModel.title, + modifier = Modifier + .padding(bottom = dimensionResource(id = R.dimen.classrooms_card_icon_padding_bottom)) + .size(size = dimensionResource(id = R.dimen.classrooms_card_icon_size)), + ) + } + Text( + text = classroomSummaryViewModel.title, + color = colorResource(id = R.color.component_color_classroom_card_text_color), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = dimensionResource(id = R.dimen.classrooms_card_label_text_size).value.sp, + ) + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/promotedlist/PromotedList.kt b/app/src/main/java/org/oppia/android/app/classroom/promotedlist/PromotedList.kt new file mode 100644 index 00000000000..3ac2bdebf1d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/promotedlist/PromotedList.kt @@ -0,0 +1,233 @@ +package org.oppia.android.app.classroom.promotedlist + +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.oppia.android.R +import org.oppia.android.app.classroom.getDrawableResource +import org.oppia.android.app.home.promotedlist.PromotedStoryListViewModel +import org.oppia.android.app.home.promotedlist.PromotedStoryViewModel +import org.oppia.android.util.locale.OppiaLocale + +/** Test tag for the header of the promoted story list. */ +const val PROMOTED_STORY_LIST_HEADER_TEST_TAG = "TEST_TAG.promoted_story_list_header" + +/** Test tag for the promoted story list. */ +const val PROMOTED_STORY_LIST_TEST_TAG = "TEST_TAG.promoted_story_list" + +/** Displays a list of promoted stories. */ +@Composable +fun PromotedStoryList( + promotedStoryListViewModel: PromotedStoryListViewModel, + machineLocale: OppiaLocale.MachineLocale, +) { + Row( + modifier = Modifier + .testTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG) + .fillMaxWidth() + .padding( + start = dimensionResource(id = R.dimen.promoted_story_list_layout_margin_start), + top = dimensionResource(id = R.dimen.promoted_story_list_layout_margin_top), + end = dimensionResource(id = R.dimen.promoted_story_list_layout_margin_end), + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = promotedStoryListViewModel.getHeader(), + color = colorResource(id = R.color.component_color_shared_primary_text_color), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = dimensionResource(id = R.dimen.promoted_story_list_header_text_size).value.sp, + modifier = Modifier + .weight(weight = 1f, fill = false), + ) + if (promotedStoryListViewModel.getViewAllButtonVisibility() == View.VISIBLE) { + Text( + text = machineLocale.run { stringResource(id = R.string.view_all).toMachineUpperCase() }, + color = colorResource(id = R.color.component_color_home_activity_view_all_text_color), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = dimensionResource(id = R.dimen.promoted_story_list_view_all_text_size).value.sp, + modifier = Modifier + .padding( + start = dimensionResource(id = R.dimen.promoted_story_list_view_all_padding_start) + ) + .clickable { promotedStoryListViewModel.clickOnViewAll() }, + ) + } + } + LazyRow( + modifier = Modifier + .testTag(PROMOTED_STORY_LIST_TEST_TAG) + .padding(top = dimensionResource(id = R.dimen.promoted_story_list_padding)), + contentPadding = PaddingValues( + start = dimensionResource(id = R.dimen.promoted_story_list_layout_margin_start), + end = promotedStoryListViewModel.endPadding.dp, + ), + ) { + items(promotedStoryListViewModel.promotedStoryList) { + PromotedStoryCard( + promotedStoryViewModel = it, + machineLocale = machineLocale + ) + } + } +} + +/** Displays a single promoted story card with an image, title, and handling click events. */ +@Composable +fun PromotedStoryCard( + promotedStoryViewModel: PromotedStoryViewModel, + machineLocale: OppiaLocale.MachineLocale, +) { + val cardLayoutWidth = promotedStoryViewModel.computeLayoutWidth() + val cardColumnModifier = + if (cardLayoutWidth == ViewGroup.LayoutParams.MATCH_PARENT) Modifier.fillMaxWidth() + else Modifier.width(promotedStoryViewModel.computeLayoutWidth().dp) + + Card( + modifier = Modifier + .width(width = dimensionResource(id = R.dimen.promoted_story_card_layout_width)) + .padding( + start = dimensionResource(id = R.dimen.promoted_story_card_layout_margin_start), + end = dimensionResource(id = R.dimen.promoted_story_card_layout_margin_end), + bottom = dimensionResource(id = R.dimen.promoted_story_card_layout_margin_bottom), + ) + .clickable { promotedStoryViewModel.clickOnStoryTile() }, + backgroundColor = colorResource( + id = R.color.component_color_shared_screen_primary_background_color + ), + elevation = dimensionResource(id = R.dimen.promoted_story_card_elevation), + ) { + Column( + modifier = cardColumnModifier + ) { + Image( + painter = painterResource( + id = promotedStoryViewModel.promotedStory.lessonThumbnail.getDrawableResource() + ), + contentDescription = promotedStoryViewModel.storyTitle, + modifier = Modifier + .aspectRatio(16f / 9f) + .background( + Color( + ( + 0xff000000L or + promotedStoryViewModel.promotedStory.lessonThumbnail.backgroundColorRgb.toLong() + ).toInt() + ) + ) + ) + Text( + text = promotedStoryViewModel.nextChapterTitle, + modifier = Modifier.padding( + start = dimensionResource( + id = R.dimen.promoted_story_card_padding_horizontal + ), + top = dimensionResource( + id = R.dimen.promoted_story_card_padding_vertical + ), + end = dimensionResource( + id = R.dimen.promoted_story_card_padding_horizontal + ), + ), + color = colorResource(id = R.color.component_color_shared_primary_text_color), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = dimensionResource( + id = R.dimen.promoted_story_card_chapter_title_text_size + ).value.sp, + textAlign = TextAlign.Start, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = machineLocale.run { promotedStoryViewModel.topicTitle.toMachineUpperCase() }, + modifier = Modifier.padding( + start = dimensionResource( + id = R.dimen.promoted_story_card_padding_horizontal + ), + top = dimensionResource( + id = R.dimen.promoted_story_card_padding_vertical + ), + end = dimensionResource( + id = R.dimen.promoted_story_card_padding_horizontal + ), + ), + color = colorResource( + id = R.color.component_color_shared_story_card_topic_name_text_color + ), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Light, + fontSize = dimensionResource( + id = R.dimen.promoted_story_card_topic_title_text_size + ).value.sp, + textAlign = TextAlign.Start, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = machineLocale.run { promotedStoryViewModel.classroomTitle.toMachineUpperCase() }, + modifier = Modifier + .padding( + horizontal = dimensionResource(id = R.dimen.promoted_story_card_padding_horizontal), + vertical = dimensionResource(id = R.dimen.promoted_story_card_padding_vertical), + ) + .border( + width = 2.dp, + color = colorResource( + id = R.color.component_color_classroom_promoted_list_classroom_label_color + ), + shape = RoundedCornerShape(50) + ) + .padding( + horizontal = dimensionResource(id = R.dimen.promoted_story_card_padding_horizontal), + vertical = dimensionResource(id = R.dimen.promoted_story_card_padding_vertical), + ), + color = colorResource( + id = R.color.component_color_classroom_promoted_list_classroom_label_color + ), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = dimensionResource( + id = R.dimen.promoted_story_card_classroom_title_text_size + ).value.sp, + textAlign = TextAlign.Start, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/topiclist/AllTopicsHeaderText.kt b/app/src/main/java/org/oppia/android/app/classroom/topiclist/AllTopicsHeaderText.kt new file mode 100644 index 00000000000..31dec186c60 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/topiclist/AllTopicsHeaderText.kt @@ -0,0 +1,41 @@ +package org.oppia.android.app.classroom.topiclist + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import org.oppia.android.R + +/** Test tag for the all topics section header. */ +const val ALL_TOPICS_HEADER_TEST_TAG = "TEST_TAG.all_topics_header" + +/** Displays the header text for the topic list section. */ +@Composable +fun AllTopicsHeaderText() { + Text( + text = stringResource(id = R.string.select_a_topic_to_start), + color = colorResource(id = R.color.component_color_classroom_all_topics_header_text_color), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = dimensionResource(id = R.dimen.all_topics_text_size).value.sp, + modifier = Modifier + .testTag(ALL_TOPICS_HEADER_TEST_TAG) + .fillMaxWidth() + .background(colorResource(id = R.color.color_palette_classroom_topic_list_background_color)) + .padding( + start = dimensionResource(id = R.dimen.all_topics_text_margin_start), + top = dimensionResource(id = R.dimen.all_topics_text_margin_top), + end = dimensionResource(id = R.dimen.all_topics_text_margin_end), + bottom = dimensionResource(id = R.dimen.all_topics_text_margin_bottom), + ), + ) +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/topiclist/TopicCard.kt b/app/src/main/java/org/oppia/android/app/classroom/topiclist/TopicCard.kt new file mode 100644 index 00000000000..6e67f6b5e3d --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/topiclist/TopicCard.kt @@ -0,0 +1,108 @@ +package org.oppia.android.app.classroom.topiclist + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp +import org.oppia.android.R +import org.oppia.android.app.classroom.getDrawableResource +import org.oppia.android.app.home.topiclist.TopicSummaryViewModel + +/** Displays a card with the topic summary information. */ +@Composable +fun TopicCard(topicSummaryViewModel: TopicSummaryViewModel) { + Card( + modifier = Modifier + .padding( + start = dimensionResource(R.dimen.topic_card_margin_start), + end = dimensionResource(R.dimen.topic_card_margin_end), + ) + .clickable { topicSummaryViewModel.clickOnSummaryTile() }, + elevation = dimensionResource(id = R.dimen.topic_card_elevation), + ) { + Column( + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource( + id = topicSummaryViewModel.topicSummary.topicThumbnail.getDrawableResource() + ), + contentDescription = "Picture of a " + + "${topicSummaryViewModel.topicSummary.topicThumbnail.thumbnailGraphic.name}.", + modifier = Modifier + .aspectRatio(4f / 3f) + .background( + Color( + ( + 0xff000000L or + topicSummaryViewModel.topicSummary.topicThumbnail.backgroundColorRgb.toLong() + ).toInt() + ) + ) + ) + TopicCardTextSection(topicSummaryViewModel) + } + } +} + +/** Displays text section of the topic card. */ +@Composable +fun TopicCardTextSection(topicSummaryViewModel: TopicSummaryViewModel) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = colorResource( + id = R.color.component_color_shared_topic_card_item_background_color + ) + ), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = topicSummaryViewModel.title, + modifier = Modifier + .padding( + start = dimensionResource(id = R.dimen.topic_list_item_text_padding), + top = dimensionResource(id = R.dimen.topic_list_item_text_padding), + end = dimensionResource(id = R.dimen.topic_list_item_text_padding) + ), + color = colorResource(id = R.color.component_color_shared_secondary_4_text_color), + fontFamily = FontFamily.SansSerif, + fontSize = dimensionResource(id = R.dimen.topic_list_item_text_size).value.sp, + textAlign = TextAlign.Start, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = topicSummaryViewModel.computeLessonCountText(), + modifier = Modifier + .padding(all = dimensionResource(id = R.dimen.topic_list_item_text_padding)), + color = colorResource(id = R.color.component_color_shared_secondary_4_text_color), + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Light, + fontSize = dimensionResource(id = R.dimen.topic_list_item_text_size).value.sp, + fontStyle = FontStyle.Italic, + textAlign = TextAlign.Start, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/classroom/welcome/WelcomeText.kt b/app/src/main/java/org/oppia/android/app/classroom/welcome/WelcomeText.kt new file mode 100644 index 00000000000..f70bcd293b0 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/classroom/welcome/WelcomeText.kt @@ -0,0 +1,53 @@ +package org.oppia.android.app.classroom.welcome + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.oppia.android.R +import org.oppia.android.app.home.WelcomeViewModel + +/** Test tag for the welcome section. */ +const val WELCOME_TEST_TAG = "TEST_TAG.welcome" + +/** Displays a welcome text with an underline. */ +@Composable +fun WelcomeText(welcomeViewModel: WelcomeViewModel) { + val outerPadding = dimensionResource(id = R.dimen.home_welcome_outer_padding) + val textMarginEnd = dimensionResource(id = R.dimen.home_welcome_text_view_margin_end) + val greetingLineColor = colorResource( + id = R.color.component_color_home_activity_layout_greeting_text_line_color + ) + + Text( + text = welcomeViewModel.computeWelcomeText(), + modifier = Modifier + .testTag(WELCOME_TEST_TAG) + .padding( + start = outerPadding, + top = outerPadding, + end = outerPadding + textMarginEnd, + ) + .drawBehind { + val strokeWidthPx = 6.dp.toPx() + val verticalOffset = size.height + 4.dp.toPx() + drawLine( + color = greetingLineColor, + strokeWidth = strokeWidthPx, + start = Offset(x = 0f, y = verticalOffset), + end = Offset(x = size.width, y = verticalOffset), + ) + }, + color = colorResource(id = R.color.component_color_shared_primary_text_color), + fontSize = dimensionResource(id = R.dimen.home_welcome_text_size).value.sp, + fontFamily = FontFamily.SansSerif, + ) +} diff --git a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt index a8fcd95253f..f606bafca88 100644 --- a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt @@ -16,6 +16,7 @@ import com.google.android.material.navigation.NavigationView import com.google.common.base.Optional import org.oppia.android.R import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.devoptions.DeveloperOptionsStarter import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.help.HelpActivity @@ -37,6 +38,8 @@ import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import org.oppia.android.util.platformparameter.EnableMultipleClassrooms +import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.oppia.android.util.statusbar.StatusBarColor import javax.inject.Inject @@ -53,7 +56,8 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private val oppiaLogger: OppiaLogger, private val headerViewModel: NavigationDrawerHeaderViewModel, private val footerViewModel: NavigationDrawerFooterViewModel, - private val developerOptionsStarter: Optional + private val developerOptionsStarter: Optional, + @EnableMultipleClassrooms private val enableMultipleClassrooms: PlatformParameterValue, ) : NavigationView.OnNavigationItemSelectedListener { private lateinit var drawerToggle: ActionBarDrawerToggle private lateinit var drawerLayout: DrawerLayout @@ -232,7 +236,10 @@ class NavigationDrawerFragmentPresenter @Inject constructor( if (previousMenuItemId != menuItemId) { when (NavigationDrawerItem.valueFromNavId(menuItemId)) { NavigationDrawerItem.HOME -> { - val intent = HomeActivity.createHomeActivity(activity, profileId) + val intent = if (enableMultipleClassrooms.value) + ClassroomListActivity.createClassroomListActivity(activity, profileId) + else + HomeActivity.createHomeActivity(activity, profileId) fragment.activity!!.startActivity(intent) drawerLayout.closeDrawers() } 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 029d214a41a..b3b35f5f2ed 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 @@ -7,6 +7,7 @@ import org.oppia.android.app.administratorcontrols.AdministratorControlsFragment import org.oppia.android.app.administratorcontrols.LogoutDialogFragment import org.oppia.android.app.administratorcontrols.appversion.AppVersionFragment import org.oppia.android.app.administratorcontrols.learneranalytics.ProfileAndDeviceIdFragment +import org.oppia.android.app.classroom.ClassroomListFragment import org.oppia.android.app.completedstorylist.CompletedStoryListFragment import org.oppia.android.app.devoptions.DeveloperOptionsFragment import org.oppia.android.app.devoptions.forcenetworktype.ForceNetworkTypeFragment @@ -194,4 +195,5 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto fun inject(exitSurveyConfirmationDialogFragment: ExitSurveyConfirmationDialogFragment) fun inject(surveyWelcomeDialogFragment: SurveyWelcomeDialogFragment) fun inject(surveyOutroDialogFragment: SurveyOutroDialogFragment) + fun inject(classroomListFragment: ClassroomListFragment) } diff --git a/app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryClickListener.kt b/app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryClickListener.kt new file mode 100644 index 00000000000..4ce96bb5eec --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryClickListener.kt @@ -0,0 +1,9 @@ +package org.oppia.android.app.home.classroomlist + +import org.oppia.android.app.model.ClassroomSummary + +/** Listener interface for when a classroom card is clicked. */ +interface ClassroomSummaryClickListener { + /** Called when a classroom card is clicked by the user. */ + fun onClassroomSummaryClicked(classroomSummary: ClassroomSummary) +} diff --git a/app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryViewModel.kt new file mode 100644 index 00000000000..3ffb03b1b80 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryViewModel.kt @@ -0,0 +1,39 @@ +package org.oppia.android.app.home.classroomlist + +import org.oppia.android.app.home.HomeItemViewModel +import org.oppia.android.app.model.ClassroomSummary +import org.oppia.android.app.model.EphemeralClassroomSummary +import org.oppia.android.domain.translation.TranslationController +import java.util.Objects + +/** The view model corresponding to individual classroom summaries in the classroom summary RecyclerView. */ +class ClassroomSummaryViewModel( + private val classroomSummaryClickListener: ClassroomSummaryClickListener, + ephemeralClassroomSummary: EphemeralClassroomSummary, + translationController: TranslationController, +) : HomeItemViewModel() { + /** The [ClassroomSummary] retrieved from the [EphemeralClassroomSummary]. */ + val classroomSummary: ClassroomSummary = ephemeralClassroomSummary.classroomSummary + + /** Lazy-loaded title extracted using the [TranslationController]. */ + val title: String by lazy { + translationController.extractString( + ephemeralClassroomSummary.classroomSummary.classroomTitle, + ephemeralClassroomSummary.writtenTranslationContext + ) + } + + /** Handles the click event for a [ClassroomSummary] by invoking the click listener. */ + fun handleClassroomClick() { + classroomSummaryClickListener.onClassroomSummaryClicked(classroomSummary) + } + + // Overriding equals is needed so that DataProvider combine functions used in the + // ClassroomListViewModel will only rebind when the actual data in the data list changes, + // rather than when the ViewModel object changes. + override fun equals(other: Any?): Boolean { + return other is ClassroomSummaryViewModel + } + + override fun hashCode() = Objects.hash() +} diff --git a/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt b/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt index 2adceb6abea..6b300f6355d 100755 --- a/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModel.kt @@ -39,6 +39,11 @@ class PromotedStoryViewModel( promotedStory.nextChapterTitle, promotedStory.nextChapterWrittenTranslationContext ) } + val classroomTitle by lazy { + translationController.extractString( + promotedStory.classroomTitle, promotedStory.classroomWrittenTranslationContext + ) + } private val routeToTopicPlayStoryListener = activity as RouteToTopicPlayStoryListener diff --git a/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt b/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt index 18b623ab7ec..7d6be2cf97d 100644 --- a/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/mydownloads/MyDownloadsActivity.kt @@ -5,10 +5,13 @@ import android.content.Intent import android.os.Bundle import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.home.HomeActivity import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ScreenName.MY_DOWNLOADS_ACTIVITY import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName +import org.oppia.android.util.platformparameter.EnableMultipleClassrooms +import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import javax.inject.Inject @@ -17,6 +20,11 @@ import javax.inject.Inject class MyDownloadsActivity : InjectableAutoLocalizedAppCompatActivity() { @Inject lateinit var myDownloadsActivityPresenter: MyDownloadsActivityPresenter + + @Inject + @EnableMultipleClassrooms + lateinit var enableMultipleClassrooms: PlatformParameterValue + private var internalProfileId: Int = -1 override fun onCreate(savedInstanceState: Bundle?) { @@ -39,8 +47,11 @@ class MyDownloadsActivity : InjectableAutoLocalizedAppCompatActivity() { } override fun onBackPressed() { - val profileid = ProfileId.newBuilder().setInternalId(internalProfileId).build() - val intent = HomeActivity.createHomeActivity(this, profileid) + val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + val intent = if (enableMultipleClassrooms.value) + ClassroomListActivity.createClassroomListActivity(this, profileId) + else + HomeActivity.createHomeActivity(this, profileId) startActivity(intent) finish() } diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt index 0a842397a4b..cb33ecf7c0e 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageActivityPresenter.kt @@ -1,5 +1,6 @@ package org.oppia.android.app.options +import android.app.Activity.RESULT_OK import android.content.Intent import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil @@ -53,7 +54,7 @@ class AudioLanguageActivityPresenter @Inject constructor(private val activity: A putProtoExtra(MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY, result) } - activity.setResult(REQUEST_CODE_AUDIO_LANGUAGE, intent) + activity.setResult(RESULT_OK, intent) activity.finish() } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt b/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt index ff8d49a7ede..52a993a52f6 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsActivity.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity @@ -54,6 +56,8 @@ class OptionsActivity : private var isFirstOpen = true private lateinit var selectedFragment: String private var profileId: Int? = -1 + private lateinit var readingTextSizeLauncher: ActivityResultLauncher + private lateinit var audioLanguageLauncher: ActivityResultLauncher companion object { // TODO(#1655): Re-restrict access to fields in tests post-Gradle. @@ -115,26 +119,31 @@ class OptionsActivity : profileId!! ) title = resourceHandler.getStringInLocale(R.string.menu_options) - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - checkNotNull(data) { - "Expected data to be passed as an activity result for request: $requestCode." - } - when (requestCode) { - REQUEST_CODE_TEXT_SIZE -> { - val textSizeResults = data.getProtoExtra( + readingTextSizeLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK && result.data != null) { + val textSizeResults = result.data?.getProtoExtra( MESSAGE_READING_TEXT_SIZE_RESULTS_KEY, ReadingTextSizeActivityResultBundle.getDefaultInstance() ) - optionActivityPresenter.updateReadingTextSize(textSizeResults.selectedReadingTextSize) + if (textSizeResults != null) { + optionActivityPresenter.updateReadingTextSize(textSizeResults.selectedReadingTextSize) + } } - REQUEST_CODE_AUDIO_LANGUAGE -> { - val audioLanguage = data.getProtoExtra( + } + + audioLanguageLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK && result.data != null) { + val audioLanguage = result.data?.getProtoExtra( MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY, AudioLanguageActivityResultBundle.getDefaultInstance() - ).audioLanguage - optionActivityPresenter.updateAudioLanguage(audioLanguage) + )?.audioLanguage + if (audioLanguage != null) { + optionActivityPresenter.updateAudioLanguage(audioLanguage) + } } } } @@ -150,16 +159,14 @@ class OptionsActivity : } override fun routeAudioLanguageList(audioLanguage: AudioLanguage) { - startActivityForResult( - AudioLanguageActivity.createAudioLanguageActivityIntent(this, audioLanguage), - REQUEST_CODE_AUDIO_LANGUAGE + audioLanguageLauncher.launch( + AudioLanguageActivity.createAudioLanguageActivityIntent(this, audioLanguage) ) } override fun routeReadingTextSize(readingTextSize: ReadingTextSize) { - startActivityForResult( - ReadingTextSizeActivity.createReadingTextSizeActivityIntent(this, readingTextSize), - REQUEST_CODE_TEXT_SIZE + readingTextSizeLauncher.launch( + ReadingTextSizeActivity.createReadingTextSizeActivityIntent(this, readingTextSize) ) } diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt index 43a91064be6..2312e1247c2 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragment.kt @@ -21,12 +21,6 @@ const val MESSAGE_READING_TEXT_SIZE_RESULTS_KEY = "OptionsFragment.message_readi /** OnActivity result key to access [AudioLanguage] result. */ const val MESSAGE_AUDIO_LANGUAGE_RESULTS_KEY = "OptionsFragment.message_audio_language" -/** Request code for [ReadingTextSize]. */ -const val REQUEST_CODE_TEXT_SIZE = 1 - -/** Request code for [AudioLanguage]. */ -const val REQUEST_CODE_AUDIO_LANGUAGE = 3 - /** Arguments key for OptionsFragment. */ const val OPTIONS_FRAGMENT_ARGUMENTS_KEY = "OptionsFragment.arguments" diff --git a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt index 6c8c14d87ae..e88525841b5 100644 --- a/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt +++ b/app/src/main/java/org/oppia/android/app/options/ReadingTextSizeActivity.kt @@ -67,7 +67,7 @@ class ReadingTextSizeActivity : InjectableAutoLocalizedAppCompatActivity() { val intent = Intent().apply { putProtoExtra(MESSAGE_READING_TEXT_SIZE_RESULTS_KEY, resultBundle) } - setResult(REQUEST_CODE_TEXT_SIZE, intent) + setResult(RESULT_OK, intent) finish() } diff --git a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt index e90eda75749..1acff5fd581 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivity.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.profile import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.model.AddProfileActivityParams @@ -32,6 +33,14 @@ class AddProfileActivity : InjectableAutoLocalizedAppCompatActivity() { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) addProfileFragmentPresenter.handleOnCreate() + + addProfileFragmentPresenter.resultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK) { + addProfileFragmentPresenter.updateProfileAvatar(result.data) + } + } } override fun onSupportNavigateUp(): Boolean { @@ -42,11 +51,6 @@ class AddProfileActivity : InjectableAutoLocalizedAppCompatActivity() { return false } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - addProfileFragmentPresenter.handleOnActivityResult(requestCode, resultCode, data) - } - override fun onDestroy() { super.onDestroy() addProfileFragmentPresenter.dismissAlertDialog() diff --git a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt index eff1305e68e..ef00c6d337d 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt @@ -1,6 +1,5 @@ package org.oppia.android.app.profile -import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.PorterDuff @@ -10,6 +9,7 @@ import android.provider.MediaStore import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.ImageView +import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar @@ -37,8 +37,6 @@ import org.oppia.android.util.platformparameter.EnableDownloadsSupport import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject -const val GALLERY_INTENT_RESULT_CODE = 1 - /** The presenter for [AddProfileActivity]. */ @ActivityScope class AddProfileActivityPresenter @Inject constructor( @@ -55,6 +53,11 @@ class AddProfileActivityPresenter @Inject constructor( private var checkboxStateClicked = false private var inputtedConfirmPin = false private lateinit var alertDialog: AlertDialog + private val galleryIntent = Intent( + Intent.ACTION_PICK, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + ) + lateinit var resultLauncher: ActivityResultLauncher fun handleOnCreate() { val binding = DataBindingUtil.setContentView( @@ -184,25 +187,23 @@ class AddProfileActivityPresenter @Inject constructor( } } - fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == GALLERY_INTENT_RESULT_CODE && resultCode == Activity.RESULT_OK) { - data?.let { - selectedImage = data.data - Glide.with(activity) - .load(selectedImage) - .centerCrop() - .apply(RequestOptions.circleCropTransform()) - .into(uploadImageView) - } + fun updateProfileAvatar(data: Intent?) { + data?.let { + selectedImage = data.data + Glide.with(activity) + .load(selectedImage) + .centerCrop() + .apply(RequestOptions.circleCropTransform()) + .into(uploadImageView) } } private fun addButtonListeners(binding: AddProfileActivityBinding) { uploadImageView.setOnClickListener { - openGalleryIntent() + resultLauncher.launch(galleryIntent) } binding.addProfileActivityEditUserImageView.setOnClickListener { - openGalleryIntent() + resultLauncher.launch(galleryIntent) } binding.addProfileActivityCreateButton.setOnClickListener { @@ -249,11 +250,6 @@ class AddProfileActivityPresenter @Inject constructor( } } - private fun openGalleryIntent() { - val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) - activity.startActivityForResult(galleryIntent, GALLERY_INTENT_RESULT_CODE) - } - private fun checkInputsAreValid(name: String, pin: String, confirmPin: String): Boolean { var failed = false if (name.isEmpty()) { diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt index be7e104eaf4..266a88636fa 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt @@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.fragment.app.DialogFragment import org.oppia.android.R +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.home.HomeActivity import org.oppia.android.app.model.PinPasswordActivityParams import org.oppia.android.app.model.ProfileId @@ -20,6 +21,8 @@ import org.oppia.android.util.accessibility.AccessibilityService import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.platformparameter.EnableMultipleClassrooms +import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject import kotlin.system.exitProcess @@ -33,9 +36,11 @@ class PinPasswordActivityPresenter @Inject constructor( private val lifecycleSafeTimerFactory: LifecycleSafeTimerFactory, private val pinViewModel: PinPasswordViewModel, private val resourceHandler: AppLanguageResourceHandler, - private val accessibilityService: AccessibilityService + private val accessibilityService: AccessibilityService, + @EnableMultipleClassrooms private val enableMultipleClassrooms: PlatformParameterValue, ) { - private var profileId = -1 + private var internalProfileId = -1 + private var profileId = ProfileId.getDefaultInstance() private lateinit var alertDialog: AlertDialog private var confirmedDeletion = false @@ -46,13 +51,14 @@ class PinPasswordActivityPresenter @Inject constructor( ) val adminPin = args?.adminPin - profileId = args?.internalProfileId ?: -1 + internalProfileId = args?.internalProfileId ?: -1 + profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() val binding = DataBindingUtil.setContentView( activity, R.layout.pin_password_activity ) - pinViewModel.setProfileId(profileId) + pinViewModel.setProfileId(internalProfileId) binding.apply { lifecycleOwner = activity viewModel = pinViewModel @@ -95,15 +101,16 @@ class PinPasswordActivityPresenter @Inject constructor( ) { if (inputtedPin == pinViewModel.correctPin.get()) { profileManagementController - .loginToProfile( - ProfileId.newBuilder().setInternalId(profileId).build() - ).toLiveData() - .observe( + .loginToProfile(profileId).toLiveData().observe( activity, { if (it is AsyncResult.Success) { - val profileid = ProfileId.newBuilder().setInternalId(profileId).build() - activity.startActivity((HomeActivity.createHomeActivity(activity, profileid))) + activity.startActivity( + if (enableMultipleClassrooms.value) + ClassroomListActivity.createClassroomListActivity(activity, profileId) + else + HomeActivity.createHomeActivity(activity, profileId) + ) } } ) @@ -157,7 +164,7 @@ class PinPasswordActivityPresenter @Inject constructor( ) as DialogFragment ).dismiss() val dialogFragment = ResetPinDialogFragment.newInstance( - profileId, + internalProfileId, pinViewModel.name.get()!! ) dialogFragment.showNow(activity.supportFragmentManager, TAG_RESET_PIN_DIALOG) 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 a138df9b575..371bdfc9037 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 @@ -14,6 +14,7 @@ import androidx.lifecycle.Transformations import androidx.recyclerview.widget.GridLayoutManager import org.oppia.android.R import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.home.HomeActivity import org.oppia.android.app.model.Profile @@ -28,6 +29,8 @@ import org.oppia.android.domain.oppialogger.analytics.AnalyticsController import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import org.oppia.android.util.platformparameter.EnableMultipleClassrooms +import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.statusbar.StatusBarColor import javax.inject.Inject @@ -68,7 +71,8 @@ class ProfileChooserFragmentPresenter @Inject constructor( private val profileManagementController: ProfileManagementController, private val oppiaLogger: OppiaLogger, private val analyticsController: AnalyticsController, - private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory + private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, + @EnableMultipleClassrooms private val enableMultipleClassrooms: PlatformParameterValue, ) { private lateinit var binding: ProfileChooserFragmentBinding val hasProfileEverBeenAddedValue = ObservableField(true) @@ -175,14 +179,15 @@ class ProfileChooserFragmentPresenter @Inject constructor( fragment, Observer { if (it is AsyncResult.Success) { - activity.startActivity( - ( - HomeActivity.createHomeActivity( - activity, - model.profile.id - ) - ) - ) + if (enableMultipleClassrooms.value) { + activity.startActivity( + ClassroomListActivity.createClassroomListActivity(activity, model.profile.id) + ) + } else { + activity.startActivity( + HomeActivity.createHomeActivity(activity, model.profile.id) + ) + } } } ) diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt index 0dcb5c6e39f..b0066ad36f5 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivity.kt @@ -3,6 +3,8 @@ package org.oppia.android.app.profileprogress import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.activity.route.ActivityRouter @@ -38,11 +40,21 @@ class ProfileProgressActivity : @Inject lateinit var resourceHandler: AppLanguageResourceHandler + private lateinit var resultLauncher: ActivityResultLauncher + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) internalProfileId = intent?.extractCurrentUserProfileId()?.internalId ?: -1 profileProgressActivityPresenter.handleOnCreate(internalProfileId) + + resultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK) { + profileProgressActivityPresenter.updateProfileAvatar(result.data) + } + } } override fun routeToRecentlyPlayed(recentlyPlayedActivityTitle: RecentlyPlayedActivityTitle) { @@ -101,11 +113,7 @@ class ProfileProgressActivity : } override fun showGalleryForProfilePicture() { - profileProgressActivityPresenter.openGalleryIntent() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - profileProgressActivityPresenter.handleOnActivityResult(data) + val galleryIntent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "image/*" } + resultLauncher.launch(galleryIntent) } } diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivityPresenter.kt index 562e38f3d9f..836f5cfc0d5 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressActivityPresenter.kt @@ -7,7 +7,6 @@ import androidx.appcompat.widget.Toolbar import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.profile.GALLERY_INTENT_RESULT_CODE import org.oppia.android.domain.profile.ProfileManagementController import javax.inject.Inject @@ -50,18 +49,11 @@ class ProfileProgressActivityPresenter @Inject constructor( ) as ProfileProgressFragment? } - fun openGalleryIntent() { - val galleryIntent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "image/*" } - activity.startActivityForResult(galleryIntent, GALLERY_INTENT_RESULT_CODE) - } - - fun handleOnActivityResult(intent: Intent?) { - intent?.let { - profileManagementController.updateProfileAvatar( - profileId, - intent.data, - /* colorRgb= */ 10710042 - ) - } + fun updateProfileAvatar(intent: Intent?) { + profileManagementController.updateProfileAvatar( + profileId, + intent?.data, + /* colorRgb= */ 10710042 + ) } } diff --git a/app/src/main/res/drawable/ic_english.xml b/app/src/main/res/drawable/ic_english.xml new file mode 100644 index 00000000000..ebd5f248582 --- /dev/null +++ b/app/src/main/res/drawable/ic_english.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_maths.xml b/app/src/main/res/drawable/ic_maths.xml new file mode 100644 index 00000000000..0abcf106ad1 --- /dev/null +++ b/app/src/main/res/drawable/ic_maths.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_science.xml b/app/src/main/res/drawable/ic_science.xml new file mode 100644 index 00000000000..fc7b811a7ef --- /dev/null +++ b/app/src/main/res/drawable/ic_science.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/classroom_list_activity.xml b/app/src/main/res/layout/classroom_list_activity.xml new file mode 100644 index 00000000000..9143cfc29e1 --- /dev/null +++ b/app/src/main/res/layout/classroom_list_activity.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/classroom_list_fragment.xml b/app/src/main/res/layout/classroom_list_fragment.xml new file mode 100644 index 00000000000..7a69ab537b6 --- /dev/null +++ b/app/src/main/res/layout/classroom_list_fragment.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml index 578d3179178..49afd330bdf 100644 --- a/app/src/main/res/values-land/dimens.xml +++ b/app/src/main/res/values-land/dimens.xml @@ -348,6 +348,10 @@ 14sp 8dp + + 8dp + 8dp + 104dp 96dp @@ -542,4 +546,12 @@ 108dp + + + 72dp + 20dp + 72dp + 12dp + 150dp + 60dp diff --git a/app/src/main/res/values-night/color_palette.xml b/app/src/main/res/values-night/color_palette.xml index fe0d284624e..7c9347d590a 100644 --- a/app/src/main/res/values-night/color_palette.xml +++ b/app/src/main/res/values-night/color_palette.xml @@ -231,4 +231,8 @@ @color/color_def_dark_green @color/color_def_accessible_grey + + @color/color_def_greenish_black + @color/color_def_white + @color/color_def_greenish_black diff --git a/app/src/main/res/values-sw600dp-land/dimens.xml b/app/src/main/res/values-sw600dp-land/dimens.xml index d24a8c3c4e8..47caee25f05 100644 --- a/app/src/main/res/values-sw600dp-land/dimens.xml +++ b/app/src/main/res/values-sw600dp-land/dimens.xml @@ -362,6 +362,10 @@ 16sp 4dp + + 0dp + 32dp + 8dp 0dp @@ -507,4 +511,12 @@ 192dp 192dp 4dp + + + 72dp + 20dp + 72dp + 12dp + 150dp + 182dp diff --git a/app/src/main/res/values-sw600dp-port/dimens.xml b/app/src/main/res/values-sw600dp-port/dimens.xml index 325532ede04..d2c15feccbc 100644 --- a/app/src/main/res/values-sw600dp-port/dimens.xml +++ b/app/src/main/res/values-sw600dp-port/dimens.xml @@ -367,6 +367,10 @@ 16sp 4dp + + 8dp + 24dp + 8dp 0dp @@ -524,4 +528,12 @@ 72dp 32dp 8dp + + + 60dp + 20dp + 60dp + 12dp + 150dp + 182dp diff --git a/app/src/main/res/values/color_defs.xml b/app/src/main/res/values/color_defs.xml index 9fdaccaaf51..d6144b1d5ed 100644 --- a/app/src/main/res/values/color_defs.xml +++ b/app/src/main/res/values/color_defs.xml @@ -144,4 +144,6 @@ #E8E8E8 #E2F5F4 #25000000 + #EDF6F5 + #172B28 diff --git a/app/src/main/res/values/color_palette.xml b/app/src/main/res/values/color_palette.xml index 36cf4fa64ef..649cc14c2d9 100644 --- a/app/src/main/res/values/color_palette.xml +++ b/app/src/main/res/values/color_palette.xml @@ -271,4 +271,9 @@ @color/color_def_oppia_green @color/color_def_accessible_grey + + @color/color_def_greenish_white + @color/color_def_green + @color/color_def_greenish_white + @color/color_def_persian_blue diff --git a/app/src/main/res/values/component_colors.xml b/app/src/main/res/values/component_colors.xml index 3de87773381..fd03df0ed14 100644 --- a/app/src/main/res/values/component_colors.xml +++ b/app/src/main/res/values/component_colors.xml @@ -307,4 +307,11 @@ @color/color_palette_white_text_color @color/color_palette_onboarding_primary_color @color/color_palette_onboarding_primary_text_color + + @color/color_palette_classroom_card_color + @color/color_palette_classroom_shared_text_color + @color/color_palette_classroom_shared_text_color + @color/color_palette_classroom_shared_text_color + @color/color_palette_classroom_topic_list_background_color + @color/color_palette_classroom_promoted_list_classroom_label_color diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index fe44bb45857..2d53f641df9 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -387,6 +387,8 @@ 12dp + 8dp + 14sp 88dp @@ -453,6 +455,7 @@ 56dp 32dp 12dp + 18dp 16dp @@ -509,6 +512,7 @@ 28dp 28dp 4dp + 24sp @@ -527,11 +531,23 @@ 8dp 8dp + 8dp + 8dp + 16dp + 8dp + 4dp 280sp 280sp 16sp - 8dp + 18sp + 14sp + 14sp + + + 8dp + 8dp + 4dp 40dp @@ -633,6 +649,11 @@ 28dp 28dp + 24dp + 18sp + 14sp + 8sp + 12dp 132dp @@ -802,4 +823,18 @@ 16dp 28dp 36dp + + + 28dp + 20dp + 28dp + 12dp + 18sp + 150dp + 182dp + 18sp + 20dp + 20dp + 80dp + 4dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a4978ce466f..409c502e500 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -326,6 +326,9 @@ Home From now, you can see lessons recommended for you here. Select a Topic to Start + + Home + Classrooms Profiles diff --git a/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListActivityTest.kt new file mode 100644 index 00000000000..845096edd39 --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListActivityTest.kt @@ -0,0 +1,218 @@ +package org.oppia.android.app.classroom + +import android.app.Application +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.intent.Intents +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.Component +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.activity.route.ActivityRouterModule +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +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.model.ProfileId +import org.oppia.android.app.model.ScreenName +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.ExplorationProgressModule +import org.oppia.android.domain.exploration.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.firebase.TestAuthenticationModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestCoroutineDispatchers +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +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.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [ClassroomListActivity]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config( + application = ClassroomListActivityTest.TestApplication::class, + qualifiers = "port-xxhdpi" +) +class ClassroomListActivityTest { + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @Inject + lateinit var context: Context + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Before + fun setUp() { + Intents.init() + setUpTestApplicationComponent() + } + + @After + fun tearDown() { + Intents.release() + } + + @Test + fun testActivity_createIntent_verifyScreenNameInIntent() { + val screenName = ClassroomListActivity + .createClassroomListActivity( + context, + ProfileId.newBuilder().setInternalId(0).build() + ) + .extractCurrentAppScreenName() + assertThat(screenName).isEqualTo(ScreenName.CLASSROOM_LIST_ACTIVITY) + } + + @Test + fun testClassroomListActivity_hasCorrectActivityLabel() { + launchClassroomListActivity().use { scenario -> + lateinit var title: CharSequence + scenario?.onActivity { activity -> title = activity.title } + + assertThat(title).isEqualTo(context.getString(R.string.classroom_list_activity_title)) + } + } + + private fun launchClassroomListActivity(): + ActivityScenario? { + val scenario = ActivityScenario.launch( + ClassroomListActivity.createClassroomListActivity( + context, + ProfileId.newBuilder().setInternalId(0).build() + ) + ) + testCoroutineDispatchers.runCurrent() + return scenario + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + @Singleton + @Component( + modules = [ + RobolectricModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, + ApplicationStartupListenerModule::class, LogReportWorkerModule::class, + HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + SyncStatusModule::class, MetricLogSchedulerModule::class, TestingBuildFlavorModule::class, + EventLoggingConfigurationModule::class, ActivityRouterModule::class, + CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class, + TestAuthenticationModule::class + ] + ) + + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(classroomListActivityTest: ClassroomListActivityTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerClassroomListActivityTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(classroomListActivityTest: ClassroomListActivityTest) { + component.inject(classroomListActivityTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt new file mode 100644 index 00000000000..87a5c58fd0a --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt @@ -0,0 +1,848 @@ +package org.oppia.android.app.classroom + +import android.app.Application +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onChildAt +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.performScrollToNode +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.protobuf.MessageLite +import dagger.Component +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.activity.route.ActivityRouterModule +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.classroom.classroomlist.CLASSROOM_HEADER_TEST_TAG +import org.oppia.android.app.classroom.classroomlist.CLASSROOM_LIST_TEST_TAG +import org.oppia.android.app.classroom.promotedlist.PROMOTED_STORY_LIST_HEADER_TEST_TAG +import org.oppia.android.app.classroom.promotedlist.PROMOTED_STORY_LIST_TEST_TAG +import org.oppia.android.app.classroom.topiclist.ALL_TOPICS_HEADER_TEST_TAG +import org.oppia.android.app.classroom.welcome.WELCOME_TEST_TAG +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.home.recentlyplayed.RecentlyPlayedActivity +import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.TopicActivityParams +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.topic.TopicActivity +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.ExplorationProgressModule +import org.oppia.android.domain.exploration.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule +import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule +import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule +import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.FRACTIONS_STORY_ID_0 +import org.oppia.android.domain.topic.FRACTIONS_TOPIC_ID +import org.oppia.android.domain.topic.TEST_STORY_ID_0 +import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.TestImageLoaderModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor +import org.oppia.android.testing.firebase.TestAuthenticationModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.platformparameter.TestPlatformParameterModule +import org.oppia.android.testing.profile.ProfileTestHelper +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.story.StoryProgressTestHelper +import org.oppia.android.testing.threading.TestCoroutineDispatchers +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClock +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.locale.OppiaLocale +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +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.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +// Time: Tue Apr 23 2019 23:22:00 +private const val EVENING_TIMESTAMP = 1556061720000 + +// Time: Wed Apr 24 2019 08:22:00 +private const val MORNING_TIMESTAMP = 1556094120000 + +// Time: Tue Apr 23 2019 14:22:00 +private const val AFTERNOON_TIMESTAMP = 1556029320000 + +/** Tests for [ClassroomListFragment]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config( + application = ClassroomListFragmentTest.TestApplication::class, + qualifiers = "port-xxhdpi" +) +class ClassroomListFragmentTest { + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + val composeRule = createAndroidComposeRule() + + @Inject + lateinit var context: Context + + @Inject + lateinit var fakeOppiaClock: FakeOppiaClock + + @Inject + lateinit var profileTestHelper: ProfileTestHelper + + @Inject + lateinit var machineLocale: OppiaLocale.MachineLocale + + @Inject + lateinit var storyProgressTestHelper: StoryProgressTestHelper + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Inject + lateinit var dataProviderTestMonitor: DataProviderTestMonitor.Factory + + private val internalProfileId: Int = 0 + private lateinit var profileId: ProfileId + + @Before + fun setUp() { + Intents.init() + setUpTestApplicationComponent() + profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + testCoroutineDispatchers.registerIdlingResource() + profileTestHelper.initializeProfiles() + } + + @After + fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() + Intents.release() + } + + @Test + fun testFragment_allComponentsAreDisplayed() { + composeRule.onNodeWithTag(WELCOME_TEST_TAG).assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_HEADER_TEST_TAG).assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).assertIsDisplayed() + composeRule.onNodeWithTag(ALL_TOPICS_HEADER_TEST_TAG).assertIsDisplayed() + } + + @Test + fun testFragment_loginTwice_allComponentsAreDisplayed() { + logIntoAdminTwice() + composeRule.onNodeWithTag(WELCOME_TEST_TAG).assertIsDisplayed() + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).assertIsDisplayed() + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_HEADER_TEST_TAG).assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).assertIsDisplayed() + + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).performScrollToNode( + hasTestTag(ALL_TOPICS_HEADER_TEST_TAG) + ) + composeRule.onNodeWithTag(ALL_TOPICS_HEADER_TEST_TAG).assertIsDisplayed() + } + + @Test + fun testFragment_withAdminProfile_configChange_profileNameIsDisplayed() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP) + + composeRule.activity.recreate() + testCoroutineDispatchers.runCurrent() + + onView(isRoot()).perform(orientationLandscape()) + + composeRule.onNodeWithTag(WELCOME_TEST_TAG) + .assertTextContains("Good evening, Admin!") + .assertIsDisplayed() + } + + @Test + fun testFragment_morningTimestamp_goodMorningMessageIsDisplayed_withAdminProfileName() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeToSameDateTime(MORNING_TIMESTAMP) + + composeRule.activity.recreate() + testCoroutineDispatchers.runCurrent() + + composeRule.onNodeWithTag(WELCOME_TEST_TAG) + .assertTextContains("Good morning, Admin!") + .assertIsDisplayed() + } + + @Test + fun testFragment_afternoonTimestamp_goodAfternoonMessageIsDisplayed_withAdminProfileName() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeToSameDateTime(AFTERNOON_TIMESTAMP) + + composeRule.activity.recreate() + testCoroutineDispatchers.runCurrent() + + composeRule.onNodeWithTag(WELCOME_TEST_TAG) + .assertTextContains("Good afternoon, Admin!") + .assertIsDisplayed() + } + + @Test + fun testFragment_eveningTimestamp_goodEveningMessageIsDisplayed_withAdminProfileName() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP) + + composeRule.activity.recreate() + testCoroutineDispatchers.runCurrent() + + composeRule.onNodeWithTag(WELCOME_TEST_TAG) + .assertTextContains("Good evening, Admin!") + .assertIsDisplayed() + } + + @Test + fun testFragment_logUserInFirstTime_checkPromotedStoriesIsNotDisplayed() { + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).assertDoesNotExist() + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).assertDoesNotExist() + } + + @Test + fun testFragment_recentlyPlayedStoriesTextIsDisplayed() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.recently_played_stories)) + .assertIsDisplayed() + } + + @Test + fun testFragment_viewAllTextIsDisplayed() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markInProgressSavedTestTopic0Story0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(1) + .assertTextContains( + machineLocale.run { context.getString(R.string.view_all).toMachineUpperCase() } + ) + .assertIsDisplayed() + } + + @Test + fun testFragment_storiesPlayedOneWeekAgo_displaysLastPlayedStoriesText() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = true + ) + testCoroutineDispatchers.runCurrent() + storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = true + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.last_played_stories)) + .assertIsDisplayed() + } + + @Test + fun testFragment_markStory0DoneForFraction_displaysRecommendedStories() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markCompletedFractionsTopic( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.recommended_stories)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).apply { + onChildAt(0) + .assertTextContains("Prototype Exploration") + .assertTextContains("FIRST TEST TOPIC") + .assertTextContains("SCIENCE") + .assertIsDisplayed() + + onChildAt(1) + .assertTextContains("What is a Ratio?") + .assertTextContains("RATIOS AND PROPORTIONAL REASONING") + .assertTextContains("MATHS") + .assertIsDisplayed() + } + } + + @Test + fun testFragment_markCompletedRatiosStory0_recommendsFractions() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markCompletedRatiosStory0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.recommended_stories)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).onChildAt(0) + .assertTextContains("What is a Fraction?") + .assertTextContains("FRACTIONS") + .assertTextContains("MATHS") + .assertIsDisplayed() + } + + @Test + fun testFragment_noTopicProgress_initialRecommendationFractionsAndRatiosIsCorrect() { + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.recommended_stories)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).apply { + onChildAt(0) + .assertTextContains("What is a Fraction?") + .assertTextContains("FRACTIONS") + .assertTextContains("MATHS") + .assertIsDisplayed() + + onChildAt(1) + .assertTextContains("What is a Ratio?") + .assertTextContains("RATIOS AND PROPORTIONAL REASONING") + .assertTextContains("MATHS") + .assertIsDisplayed() + } + } + + @Test + fun testFragment_forPromotedActivityList_hideViewAll() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(1) + .assertDoesNotExist() + } + + @Test + @Ignore("Temporarily ignored as the test is failing.") + // TODO(#5344): Update the logic or fix the test. + fun testFragment_markStory0DoneForRatiosAndFirstTestTopic_displaysRecommendedStories() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markCompletedTestTopic0Story0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markCompletedRatiosStory0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.recommended_stories)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).onChildAt(0) + .assertTextContains("Fifth Exploration") + .assertTextContains("SECOND TEST TOPIC") + .assertTextContains("SCIENCE") + .assertIsDisplayed() + } + + /* + * # Dependency graph: + * + * Fractions + * | + * | + * v + * Test topic 0 Ratios + * \ / + * \ / + * -----> Test topic 1 <---- + * + * # Logic for recommendation system + * + * We always recommend the next topic that all dependencies are completed for. If a topic with + * prerequisites is completed out-of-order (e.g. test topic 1 above) then we assume fractions is + * already done. In the same way, finishing test topic 2 means there's nothing else to recommend. + */ + @Test + fun testFragment_markStory0DonePlayStory1FirstTestTopic_playFractionsTopic_orderIsCorrect() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markCompletedTestTopic0Story0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markInProgressSavedTestTopic1Story0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.stories_for_you)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).apply { + onChildAt(0) + .assertTextContains("Fifth Exploration") + .assertTextContains("SECOND TEST TOPIC") + .assertTextContains("SCIENCE") + .assertIsDisplayed() + + onChildAt(1) + .assertTextContains("What is a Fraction?") + .assertTextContains("FRACTIONS") + .assertTextContains("MATHS") + .assertIsDisplayed() + + // TODO(#5344): 'What is a Ratio?' story should be promoted. + onChildAt(2) + .assertDoesNotExist() + } + } + + @Test + @Ignore + // TODO(#5344): Update logic or fix the test. + fun testFragment_markStory0DoneFirstTestTopic_recommendedStoriesIsCorrect() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markCompletedTestTopic0Story0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.recommended_stories)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).onChildAt(1) + .assertTextContains("What is a Ratio?") + .assertTextContains("RATIOS AND PROPORTIONAL REASONING") + .assertTextContains("MATHS") + .assertIsDisplayed() + } + + @Test + fun testFragment_markStory0DoneForFractions_recommendedStoriesIsCorrect() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markCompletedFractionsStory0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.activity.recreate() + testCoroutineDispatchers.runCurrent() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.recommended_stories)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).apply { + onChildAt(0) + .assertTextContains("Prototype Exploration") + .assertTextContains("FIRST TEST TOPIC") + .assertTextContains("SCIENCE") + .assertIsDisplayed() + + onChildAt(1) + .assertTextContains("What is a Ratio?") + .assertTextContains("RATIOS AND PROPORTIONAL REASONING") + .assertTextContains("MATHS") + .assertIsDisplayed() + } + } + + @Test + fun testFragment_clickViewAll_opensRecentlyPlayedActivity() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markInProgressSavedTestTopic1( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(1) + .assertIsDisplayed() + .performClick() + + intended(hasComponent(RecentlyPlayedActivity::class.java.name)) + } + + @Test + fun testFragment_markFullProgressForFractions_playRatios_displaysRecommendedStories() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + storyProgressTestHelper.markCompletedFractionsStory0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0) + .assertTextContains(context.getString(R.string.stories_for_you)) + .assertIsDisplayed() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).apply { + onChildAt(0) + .assertTextContains("What is a Ratio?") + .assertTextContains("RATIOS AND PROPORTIONAL REASONING") + .assertTextContains("MATHS") + .assertIsDisplayed() + + onChildAt(1) + .assertTextContains("Prototype Exploration") + .assertTextContains("FIRST TEST TOPIC") + .assertTextContains("SCIENCE") + .assertIsDisplayed() + } + } + + @Test + fun testFragment_clickPromotedStory_opensTopicActivity() { + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( + profileId = profileId, + timestampOlderThanOneWeek = false + ) + logIntoAdminTwice() + + composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).onChildAt(0) + .assertIsDisplayed() + .performClick() + + testCoroutineDispatchers.runCurrent() + + val args = TopicActivityParams.newBuilder().apply { + this.topicId = FRACTIONS_TOPIC_ID + this.storyId = FRACTIONS_STORY_ID_0 + }.build() + intended(hasComponent(TopicActivity::class.java.name)) + intended(hasProtoExtra(TopicActivity.TOPIC_ACTIVITY_PARAMS_KEY, args)) + } + + @Test + fun testFragment_clickTopicSummary_opensTopicActivityThroughPlayIntent() { + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(0).performClick() + testCoroutineDispatchers.runCurrent() + + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("First Test Topic") + .assertTextContains("3 Lessons") + .assertIsDisplayed() + .performClick() + + testCoroutineDispatchers.runCurrent() + + val args = TopicActivityParams.newBuilder().apply { + this.topicId = TEST_TOPIC_ID_0 + this.storyId = TEST_STORY_ID_0 + }.build() + intended(hasComponent(TopicActivity::class.java.name)) + intended(hasProtoExtra(TopicActivity.TOPIC_ACTIVITY_PARAMS_KEY, args)) + } + + @Test + fun testFragment_scrollToBottom_classroomListSticks_classroomListIsVisible() { + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).performScrollToIndex(3) + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).assertIsDisplayed() + } + + @Test + fun testFragment_switchClassroom_topicListUpdatesCorrectly() { + // Click on Science classroom card. + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(0).performClick() + testCoroutineDispatchers.runCurrent() + + // Check that Science classroom's topics are displayed. + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("First Test Topic") + .assertTextContains("3 Lessons") + .assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(5) + .assertTextContains("Second Test Topic") + .assertTextContains("1 Lesson") + .assertIsDisplayed() + + // Click on Maths classroom card. + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(1).performClick() + testCoroutineDispatchers.runCurrent() + + // Check that Maths classroom's topics are displayed. + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("Fractions") + .assertTextContains("2 Lessons") + .assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(5) + .assertTextContains("Ratios and Proportional Reasoning") + .assertTextContains("4 Lessons") + .assertIsDisplayed() + } + + @Test + fun testFragment_clickOnTopicCard_returnBack_classroomSelectionIsRetained() { + // Click on Maths classroom card. + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(1).performClick() + testCoroutineDispatchers.runCurrent() + + // Check that Fractions topic is displayed and perform click. + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("Fractions") + .assertTextContains("2 Lessons") + .assertIsDisplayed() + .performClick() + + pressBack() + + // Check that Maths classroom is selected & its topics are displayed. + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("Fractions") + .assertTextContains("2 Lessons") + .assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(5) + .assertTextContains("Ratios and Proportional Reasoning") + .assertTextContains("4 Lessons") + .assertIsDisplayed() + } + + @Test + fun testFragment_switchClassrooms_topicListUpdatesCorrectly() { + profileTestHelper.logIntoAdmin() + testCoroutineDispatchers.runCurrent() + + // Click on Science classroom card. + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(0).performClick() + testCoroutineDispatchers.runCurrent() + // Check that Science classroom's topics are displayed. + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("First Test Topic") + .assertTextContains("3 Lessons") + .assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(5) + .assertTextContains("Second Test Topic") + .assertTextContains("1 Lesson") + .assertIsDisplayed() + + // Click on Maths classroom card. + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(1).performClick() + testCoroutineDispatchers.runCurrent() + // Check that Maths classroom's topics are displayed. + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("Fractions") + .assertTextContains("2 Lessons") + .assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(5) + .assertTextContains("Ratios and Proportional Reasoning") + .assertTextContains("4 Lessons") + .assertIsDisplayed() + + // Click on Science classroom card. + composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(0).performClick() + testCoroutineDispatchers.runCurrent() + // Check that Science classroom's topics are displayed. + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(4) + .assertTextContains("First Test Topic") + .assertTextContains("3 Lessons") + .assertIsDisplayed() + composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).onChildAt(5) + .assertTextContains("Second Test Topic") + .assertTextContains("1 Lesson") + .assertIsDisplayed() + } + + private fun logIntoAdminTwice() { + dataProviderTestMonitor.waitForNextSuccessfulResult(profileTestHelper.logIntoAdmin()) + dataProviderTestMonitor.waitForNextSuccessfulResult(profileTestHelper.logIntoAdmin()) + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private fun hasProtoExtra(keyName: String, expectedProto: T): Matcher { + val defaultProto = expectedProto.newBuilderForType().build() + return object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("Intent with extra: $keyName and proto value: $expectedProto") + } + + override fun matchesSafely(intent: Intent): Boolean { + return intent.hasExtra(keyName) && + intent.getProtoExtra(keyName, defaultProto) == expectedProto + } + } + } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + @Singleton + @Component( + modules = [ + TestPlatformParameterModule::class, RobolectricModule::class, + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, + ApplicationStartupListenerModule::class, LogReportWorkerModule::class, + HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + SyncStatusModule::class, MetricLogSchedulerModule::class, TestingBuildFlavorModule::class, + EventLoggingConfigurationModule::class, ActivityRouterModule::class, + CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class, + TestAuthenticationModule::class, TestImageLoaderModule::class, + ] + ) + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(classroomListFragmentTest: ClassroomListFragmentTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerClassroomListFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(classroomListFragmentTest: ClassroomListFragmentTest) { + component.inject(classroomListFragmentTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt index ce7efa5e687..98eee4d1a68 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt @@ -5,9 +5,15 @@ import android.content.Context import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.Component +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -22,8 +28,10 @@ import org.oppia.android.app.application.ApplicationInjectorProvider import org.oppia.android.app.application.ApplicationModule import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.home.HomeActivity import org.oppia.android.app.model.ScreenName import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule @@ -55,14 +63,16 @@ import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.RunOn import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.TestPlatform import org.oppia.android.testing.firebase.TestAuthenticationModule import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.platformparameter.TestPlatformParameterModule import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -81,6 +91,7 @@ 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.profile.PROFILE_ID_INTENT_DECORATOR import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -102,9 +113,15 @@ class MyDownloadsActivityTest { @Before fun setUp() { + Intents.init() setUpTestApplicationComponent() } + @After + fun tearDown() { + Intents.release() + } + private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) } @@ -133,11 +150,32 @@ class MyDownloadsActivityTest { assertThat(screenName).isEqualTo(ScreenName.MY_DOWNLOADS_ACTIVITY) } + @Test + @RunOn(TestPlatform.ESPRESSO) + fun testMyDownloadsActivity_classroomsFlagDisabled_pressBack_opensHomeActivity() { + ActivityScenario.launch(MyDownloadsActivity::class.java).use { + pressBack() + intended(hasComponent(HomeActivity::class.java.name)) + hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR) + } + } + + @Test + @RunOn(TestPlatform.ESPRESSO) + fun testMyDownloadsActivity_classroomsFlagEnabled_pressBack_opensClassroomListActivity() { + TestPlatformParameterModule.forceEnableMultipleClassrooms(true) + ActivityScenario.launch(MyDownloadsActivity::class.java).use { + pressBack() + intended(hasComponent(ClassroomListActivity::class.java.name)) + hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR) + } + } + @Singleton @Component( modules = [ RobolectricModule::class, - PlatformParameterModule::class, PlatformParameterSingletonModule::class, + TestPlatformParameterModule::class, PlatformParameterSingletonModule::class, TestDispatcherModule::class, ApplicationModule::class, LoggerModule::class, ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt index 54751fd2ac0..5da1ccdf4e5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt @@ -13,6 +13,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.hasFocus import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA @@ -45,6 +46,7 @@ import org.oppia.android.app.application.ApplicationInjectorProvider import org.oppia.android.app.application.ApplicationModule import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.home.HomeActivity @@ -81,17 +83,19 @@ import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.RunOn import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.TestPlatform import org.oppia.android.testing.espresso.EditTextInputAction import org.oppia.android.testing.espresso.TextInputAction.Companion.hasErrorText import org.oppia.android.testing.espresso.TextInputAction.Companion.hasNoErrorText import org.oppia.android.testing.firebase.TestAuthenticationModule import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.platformparameter.TestPlatformParameterModule import org.oppia.android.testing.profile.ProfileTestHelper import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers @@ -113,6 +117,7 @@ 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.profile.PROFILE_ID_INTENT_DECORATOR import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -229,6 +234,26 @@ class PinPasswordActivityTest { } } + @Test + @RunOn(TestPlatform.ESPRESSO) + fun testPinPassword_enableClassrooms_withAdmin_inputCorrectPin_opensClassroomListActivity() { + TestPlatformParameterModule.forceEnableMultipleClassrooms(true) + ActivityScenario.launch( + PinPasswordActivity.createPinPasswordActivityIntent( + context = context, + adminPin = adminPin, + profileId = adminId + ) + ).use { + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.pin_password_input_pin_edit_text)) + .perform(editTextInputAction.appendText("12345")) + testCoroutineDispatchers.runCurrent() + intended(hasComponent(ClassroomListActivity::class.java.name)) + hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR) + } + } + @Test fun testPinPassword_withUser_inputCorrectPin_opensHomeActivity() { ActivityScenario.launch( @@ -246,6 +271,26 @@ class PinPasswordActivityTest { } } + @Test + @RunOn(TestPlatform.ESPRESSO) + fun testPinPassword_enableClassrooms_withUser_inputCorrectPin_opensClassroomListActivity() { + TestPlatformParameterModule.forceEnableMultipleClassrooms(true) + ActivityScenario.launch( + PinPasswordActivity.createPinPasswordActivityIntent( + context = context, + adminPin = adminPin, + profileId = userId + ) + ).use { + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.pin_password_input_pin_edit_text)) + .perform(editTextInputAction.appendText("123")) + testCoroutineDispatchers.runCurrent() + intended(hasComponent(ClassroomListActivity::class.java.name)) + hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR) + } + } + @Test fun testPinPassword_withAdmin_inputWrongPin_incorrectPinShows() { ActivityScenario.launch( @@ -1194,7 +1239,7 @@ class PinPasswordActivityTest { @Singleton @Component( modules = [ - RobolectricModule::class, PlatformParameterModule::class, TestDispatcherModule::class, + RobolectricModule::class, TestPlatformParameterModule::class, TestDispatcherModule::class, ApplicationModule::class, LoggerModule::class, ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt index a3ec9ef99e5..b24ca39d366 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt @@ -15,7 +15,6 @@ import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent -import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtraWithKey import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot @@ -46,8 +45,10 @@ import org.oppia.android.app.application.ApplicationInjectorProvider import org.oppia.android.app.application.ApplicationModule import org.oppia.android.app.application.ApplicationStartupListenerModule import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.classroom.ClassroomListActivity import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.home.HomeActivity import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.profile.AdminAuthActivity.Companion.ADMIN_AUTH_ACTIVITY_PARAMS_KEY import org.oppia.android.app.profile.AdminPinActivity.Companion.ADMIN_PIN_ACTIVITY_PARAMS_KEY @@ -83,15 +84,17 @@ import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.RunOn import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.TestPlatform import org.oppia.android.testing.firebase.TestAuthenticationModule import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.platformparameter.TestPlatformParameterModule import org.oppia.android.testing.profile.ProfileTestHelper import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers @@ -113,6 +116,7 @@ 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.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId +import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -486,6 +490,57 @@ class ProfileChooserFragmentTest { } } + @Test + @RunOn(TestPlatform.ESPRESSO) + fun testProfileChooserFragment_clickProfile_opensHomeActivity() { + profileManagementController.addProfile( + name = "Admin", + pin = "", + avatarImagePath = null, + allowDownloadAccess = true, + colorRgb = -10710042, + isAdmin = true + ) + launch(createProfileChooserActivityIntent()).use { + testCoroutineDispatchers.runCurrent() + onView( + atPositionOnView( + recyclerViewId = R.id.profile_recycler_view, + position = 0, + targetViewId = R.id.profile_chooser_item + ) + ).perform(click()) + intended(hasComponent(HomeActivity::class.java.name)) + hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR) + } + } + + @Test + @RunOn(TestPlatform.ESPRESSO) + fun testProfileChooserFragment_enableClassrooms_clickProfile_opensClassroomListActivity() { + TestPlatformParameterModule.forceEnableMultipleClassrooms(true) + profileManagementController.addProfile( + name = "Admin", + pin = "", + avatarImagePath = null, + allowDownloadAccess = true, + colorRgb = -10710042, + isAdmin = true + ) + launch(createProfileChooserActivityIntent()).use { + testCoroutineDispatchers.runCurrent() + onView( + atPositionOnView( + recyclerViewId = R.id.profile_recycler_view, + position = 0, + targetViewId = R.id.profile_chooser_item + ) + ).perform(click()) + intended(hasComponent(ClassroomListActivity::class.java.name)) + hasExtraWithKey(PROFILE_ID_INTENT_DECORATOR) + } + } + private fun createProfileChooserActivityIntent(): Intent { return ProfileChooserActivity .createProfileChooserActivity(ApplicationProvider.getApplicationContext()) @@ -532,7 +587,7 @@ class ProfileChooserFragmentTest { @Component( modules = [ RobolectricModule::class, - PlatformParameterModule::class, PlatformParameterSingletonModule::class, + TestPlatformParameterModule::class, PlatformParameterSingletonModule::class, TestDispatcherModule::class, ApplicationModule::class, LoggerModule::class, ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, diff --git a/app/src/test/AndroidManifest.xml b/app/src/test/AndroidManifest.xml index 7e21885881d..3d2119fd3f3 100644 --- a/app/src/test/AndroidManifest.xml +++ b/app/src/test/AndroidManifest.xml @@ -1,5 +1,5 @@ - diff --git a/build.gradle b/build.gradle index 97172637258..11bc8b058d6 100644 --- a/build.gradle +++ b/build.gradle @@ -3,12 +3,13 @@ buildscript { ext.kotlin_version = '1.6.10' ext.fragment_version = '1.2.0-rc01' + ext.compose_version = '1.1.1' repositories { google() gradlePluginPortal() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17' classpath 'com.google.gms:google-services:4.3.3' diff --git a/build_flavors.bzl b/build_flavors.bzl index 0440b171a55..68c4b6f469a 100644 --- a/build_flavors.bzl +++ b/build_flavors.bzl @@ -59,7 +59,7 @@ _FLAVOR_METADATA = { }, "dev_kitkat": { "manifest": "//app:src/main/AndroidManifest.xml", - "min_sdk_version": 19, + "min_sdk_version": 21, "target_sdk_version": 33, "multidex": "manual_main_dex", "main_dex_list": _MAIN_DEX_LIST_TARGET_KITKAT, @@ -88,7 +88,7 @@ _FLAVOR_METADATA = { }, "alpha_kitkat": { "manifest": "//app:src/main/AndroidManifest.xml", - "min_sdk_version": 19, + "min_sdk_version": 21, "target_sdk_version": 33, "multidex": "manual_main_dex", "main_dex_list": _MAIN_DEX_LIST_TARGET_KITKAT, diff --git a/data/build.gradle b/data/build.gradle index 103cea56f3b..daadba86bb8 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -4,10 +4,10 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 - buildToolsVersion "29.0.2" + buildToolsVersion "30.0.2" defaultConfig { - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -77,6 +77,7 @@ dependencies { ) testImplementation( 'androidx.test.ext:junit:1.1.1', + 'androidx.test.ext:truth:1.4.0', 'com.google.dagger:dagger:2.41', 'com.google.truth:truth:1.1.3', 'com.google.truth.extensions:truth-liteproto-extension:1.1.3', diff --git a/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt b/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt index 4a842cbe7aa..e30664000e8 100644 --- a/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt +++ b/data/src/test/java/org/oppia/android/data/backends/gae/NetworkModuleTest.kt @@ -43,36 +43,18 @@ class NetworkModuleTest { assertThat(getTestApplication().getRetrofit()).isPresent() } - @Test - @Config(sdk = [Build.VERSION_CODES.KITKAT]) - fun testRetrofitInstance_kitkat_isEmpty() { - assertThat(getTestApplication().getRetrofit()).isAbsent() - } - @Test @Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) fun testFeedbackReportingService_lollipop_isProvided() { assertThat(getTestApplication().getFeedbackReportingService()).isPresent() } - @Test - @Config(sdk = [Build.VERSION_CODES.KITKAT]) - fun testFeedbackReportingService_kitkat_isEmpty() { - assertThat(getTestApplication().getFeedbackReportingService()).isAbsent() - } - @Test @Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) fun testPlatformParameterService_lollipop_isProvided() { assertThat(getTestApplication().getPlatformParameterService()).isPresent() } - @Test - @Config(sdk = [Build.VERSION_CODES.KITKAT]) - fun testPlatformParameterService_kitkat_isEmpty() { - assertThat(getTestApplication().getPlatformParameterService()).isAbsent() - } - @Test fun testNetworkApiKey_isEmpty() { // The network API key is empty by default on developer builds. diff --git a/domain/build.gradle b/domain/build.gradle index 0d35db61ce2..5634edfa80f 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -4,10 +4,10 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 - buildToolsVersion "29.0.2" + buildToolsVersion "30.0.2" defaultConfig { - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -107,6 +107,7 @@ dependencies { testImplementation( 'androidx.test.espresso:espresso-core:3.2.0', 'androidx.test.ext:junit:1.1.1', + 'androidx.test.ext:truth:1.4.0', 'androidx.work:work-testing:2.4.0', 'com.google.dagger:dagger:2.41', 'com.google.truth.extensions:truth-liteproto-extension:1.1.3', 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 index 69c2acffa78..80175c999e5 100644 --- a/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt +++ b/domain/src/main/java/org/oppia/android/domain/classroom/ClassroomController.kt @@ -382,4 +382,4 @@ internal fun createClassroomThumbnail2(): LessonThumbnail { .build() } -private fun String?.isNotNullOrEmpty(): Boolean = !this.isNullOrBlank() || this != "null" +private fun String?.isNotNullOrEmpty(): Boolean = !this.isNullOrBlank() && this != "null" diff --git a/domain/src/test/AndroidManifest.xml b/domain/src/test/AndroidManifest.xml index 852db61ae7b..30e725f93fa 100644 --- a/domain/src/test/AndroidManifest.xml +++ b/domain/src/test/AndroidManifest.xml @@ -1,6 +1,6 @@ - diff --git a/instrumentation/BUILD.bazel b/instrumentation/BUILD.bazel index f6ffffcdc8a..d252c301da9 100644 --- a/instrumentation/BUILD.bazel +++ b/instrumentation/BUILD.bazel @@ -19,7 +19,7 @@ android_binary( manifest = "//instrumentation:src/java/AndroidManifest.xml", manifest_values = { "applicationId": "org.oppia.android", - "minSdkVersion": "19", + "minSdkVersion": "21", "targetSdkVersion": "33", "versionCode": "0", "versionName": "0.1-test", diff --git a/model/src/main/proto/screens.proto b/model/src/main/proto/screens.proto index e0ee3599d6d..84518cbea6d 100644 --- a/model/src/main/proto/screens.proto +++ b/model/src/main/proto/screens.proto @@ -158,6 +158,9 @@ enum ScreenName { // Screen name value for the scenario when the survey activity is visible to the user. SURVEY_ACTIVITY = 49; + + // Screen name value for the scenario when the classroom list activity is visible to the user. + CLASSROOM_LIST_ACTIVITY = 50; } // Defines the current visible UI screen of the application. diff --git a/scripts/assets/maven_dependencies.textproto b/scripts/assets/maven_dependencies.textproto index 4f046941d1e..b5a03249897 100644 --- a/scripts/assets/maven_dependencies.textproto +++ b/scripts/assets/maven_dependencies.textproto @@ -1,6 +1,6 @@ maven_dependency { - artifact_name: "androidx.activity:activity:1.1.0" - artifact_version: "1.1.0" + artifact_name: "androidx.activity:activity-compose:1.4.0" + artifact_version: "1.4.0" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -10,8 +10,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.annotation:annotation:1.1.0" - artifact_version: "1.1.0" + artifact_name: "androidx.activity:activity-ktx:1.4.0" + artifact_version: "1.4.0" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -21,8 +21,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.annotation:annotation-experimental:1.0.0" - artifact_version: "1.0.0" + artifact_name: "androidx.activity:activity:1.4.0" + artifact_version: "1.4.0" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -32,7 +32,7 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.appcompat:appcompat-resources:1.2.0" + artifact_name: "androidx.annotation:annotation:1.2.0" artifact_version: "1.2.0" license { license_name: "The Apache Software License, Version 2.0" @@ -43,8 +43,30 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.appcompat:appcompat:1.2.0" - artifact_version: "1.2.0" + artifact_name: "androidx.annotation:annotation-experimental:1.1.0" + artifact_version: "1.1.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.appcompat:appcompat-resources:1.3.1" + artifact_version: "1.3.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.appcompat:appcompat:1.3.1" + artifact_version: "1.3.1" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -75,6 +97,17 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "androidx.autofill:autofill:1.0.0" + artifact_version: "1.0.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} maven_dependency { artifact_name: "androidx.cardview:cardview:1.0.0" artifact_version: "1.0.0" @@ -97,6 +130,193 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "androidx.compose.animation:animation-core:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.animation:animation:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.compiler:compiler:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.foundation:foundation-layout:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.foundation:foundation:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.material:material-icons-core:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.material:material-ripple:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.material:material:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.runtime:runtime-saveable:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.runtime:runtime:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.ui:ui-geometry:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.ui:ui-graphics:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.ui:ui-text:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.ui:ui-unit:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.ui:ui-util:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.compose.ui:ui:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.concurrent:concurrent-futures:1.0.0" + artifact_version: "1.0.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} maven_dependency { artifact_name: "androidx.constraintlayout:constraintlayout-solver:2.0.1" artifact_version: "2.0.1" @@ -131,8 +351,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.core:core-ktx:1.0.1" - artifact_version: "1.0.1" + artifact_name: "androidx.core:core-ktx:1.1.0" + artifact_version: "1.1.0" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -142,8 +362,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.core:core:1.3.1" - artifact_version: "1.3.1" + artifact_name: "androidx.core:core:1.7.0" + artifact_version: "1.7.0" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -252,8 +472,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.fragment:fragment:1.2.0" - artifact_version: "1.2.0" + artifact_name: "androidx.fragment:fragment:1.3.6" + artifact_version: "1.3.6" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -285,8 +505,19 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.lifecycle:lifecycle-common:2.2.0" - artifact_version: "2.2.0" + artifact_name: "androidx.lifecycle:lifecycle-common:2.3.1" + artifact_version: "2.3.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.lifecycle:lifecycle-common-java8:2.3.0" + artifact_version: "2.3.0" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -318,8 +549,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.lifecycle:lifecycle-livedata-core:2.2.0" - artifact_version: "2.2.0" + artifact_name: "androidx.lifecycle:lifecycle-livedata-core:2.3.1" + artifact_version: "2.3.1" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -362,8 +593,19 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.lifecycle:lifecycle-runtime:2.2.0" - artifact_version: "2.2.0" + artifact_name: "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" + artifact_version: "2.3.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.lifecycle:lifecycle-runtime:2.3.1" + artifact_version: "2.3.1" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -384,8 +626,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0" - artifact_version: "1.0.0" + artifact_name: "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" + artifact_version: "2.3.1" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -395,8 +637,19 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.lifecycle:lifecycle-viewmodel:2.2.0" - artifact_version: "2.2.0" + artifact_name: "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1" + artifact_version: "2.3.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.lifecycle:lifecycle-viewmodel:2.3.1" + artifact_version: "2.3.1" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -449,6 +702,17 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "androidx.profileinstaller:profileinstaller:1.1.0" + artifact_version: "1.1.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} maven_dependency { artifact_name: "androidx.recyclerview:recyclerview:1.1.0" artifact_version: "1.1.0" @@ -483,8 +747,19 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.savedstate:savedstate:1.0.0" - artifact_version: "1.0.0" + artifact_name: "androidx.savedstate:savedstate-ktx:1.1.0" + artifact_version: "1.1.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.savedstate:savedstate:1.1.0" + artifact_version: "1.1.0" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" @@ -515,6 +790,28 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "androidx.startup:startup-runtime:1.0.0" + artifact_version: "1.0.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.tracing:tracing:1.0.0" + artifact_version: "1.0.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} maven_dependency { artifact_name: "androidx.transition:transition:1.2.0" artifact_version: "1.2.0" @@ -549,8 +846,8 @@ maven_dependency { } } maven_dependency { - artifact_name: "androidx.versionedparcelable:versionedparcelable:1.1.0" - artifact_version: "1.1.0" + artifact_name: "androidx.versionedparcelable:versionedparcelable:1.1.1" + artifact_version: "1.1.1" license { license_name: "The Apache Software License, Version 2.0" original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index a84bd17397f..6721dcb339e 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -258,6 +258,38 @@ test_file_exemption { exempted_file_path: "app/src/main/java/org/oppia/android/app/application/ga/GaApplicationComponent.kt" test_file_not_required: true } +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivityPresenter.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/ClassroomListViewModel.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/classroomlist/ClassroomList.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/promotedlist/PromotedList.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/topiclist/AllTopicsHeaderText.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/topiclist/TopicCard.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/classroom/welcome/WelcomeText.kt" + test_file_not_required: true +} test_file_exemption { exempted_file_path: "app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt" test_file_not_required: true @@ -854,6 +886,14 @@ test_file_exemption { exempted_file_path: "app/src/main/java/org/oppia/android/app/home/UserAppHistoryViewModel.kt" test_file_not_required: true } +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryClickListener.kt" + test_file_not_required: true +} +test_file_exemption { + exempted_file_path: "app/src/main/java/org/oppia/android/app/home/classroomlist/ClassroomSummaryViewModel.kt" + test_file_not_required: true +} test_file_exemption { exempted_file_path: "app/src/main/java/org/oppia/android/app/home/promotedlist/ComingSoonTopicListViewModel.kt" test_file_not_required: true diff --git a/testing/build.gradle b/testing/build.gradle index 26f050e48ef..94f2e559dff 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -4,10 +4,10 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 - buildToolsVersion "29.0.2" + buildToolsVersion "30.0.2" defaultConfig { - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -71,6 +71,7 @@ dependencies { "androidx.test:core:1.0.0", 'androidx.test.espresso:espresso-accessibility:3.1.0', 'androidx.test.espresso:espresso-core:3.2.0', + 'androidx.test.ext:truth:1.4.0', 'androidx.test:runner:1.2.0', 'com.google.android.material:material:1.3.0', 'com.google.dagger:dagger:2.41', diff --git a/testing/src/test/AndroidManifest.xml b/testing/src/test/AndroidManifest.xml index 4682f548b88..7131a53dd2f 100644 --- a/testing/src/test/AndroidManifest.xml +++ b/testing/src/test/AndroidManifest.xml @@ -1,5 +1,5 @@ - diff --git a/third_party/maven_install.json b/third_party/maven_install.json index c9a27f18450..e05d3b9c565 100644 --- a/third_party/maven_install.json +++ b/third_party/maven_install.json @@ -1,12 +1,19 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": 1041130706, - "__RESOLVED_ARTIFACTS_HASH": 1257218600, + "__INPUT_ARTIFACTS_HASH": 1471454102, + "__RESOLVED_ARTIFACTS_HASH": 1994124852, "conflict_resolution": { + "androidx.annotation:annotation:1.1.0": "androidx.annotation:annotation:1.2.0", "androidx.constraintlayout:constraintlayout:1.1.3": "androidx.constraintlayout:constraintlayout:2.0.1", - "androidx.core:core:1.0.1": "androidx.core:core:1.3.1", + "androidx.core:core-ktx:1.0.1": "androidx.core:core-ktx:1.1.0", + "androidx.core:core:1.0.1": "androidx.core:core:1.7.0", + "androidx.lifecycle:lifecycle-livedata-core:2.2.0": "androidx.lifecycle:lifecycle-livedata-core:2.3.1", + "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0": "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1", "androidx.recyclerview:recyclerview:1.0.0": "androidx.recyclerview:recyclerview:1.1.0", + "androidx.test.espresso:espresso-core:3.2.0": "androidx.test.espresso:espresso-core:3.3.0", + "androidx.test.ext:junit:1.1.1": "androidx.test.ext:junit:1.1.2", "androidx.test:core:1.0.0": "androidx.test:core:1.4.0", + "androidx.test:runner:1.2.0": "androidx.test:runner:1.3.0", "com.google.firebase:firebase-common:19.3.0": "com.google.firebase:firebase-common:20.1.1", "com.google.protobuf:protobuf-javalite:3.17.3": "com.google.protobuf:protobuf-javalite:3.19.2", "com.google.truth:truth:0.43": "com.google.truth:truth:1.1.3", @@ -16,35 +23,47 @@ "org.mockito:mockito-core:2.19.0": "org.mockito:mockito-core:3.9.0" }, "artifacts": { + "androidx.activity:activity-compose:aar": { + "shasums": { + "jar": "82f97c1c4b96d15ee721b8204bd1273f80bcc654e50d0b16ca5399c77f6c3531" + }, + "version": "1.4.0" + }, + "androidx.activity:activity-ktx:aar": { + "shasums": { + "jar": "3f301941f37a90b4bc553dbbe84e7464a97c0d21df6cf2d6c0cb1b2c07349f33" + }, + "version": "1.4.0" + }, "androidx.activity:activity:aar": { "shasums": { - "jar": "4f2b35916768032f7d0c20e250e28b29037ed4ce9ebf3da4fcd51bcb0c6067ef" + "jar": "89dc38e0cdbd11f328c7d0b3b021ddb387ca9da0d49f14b18c91e300c45ed79c" }, - "version": "1.1.0" + "version": "1.4.0" }, "androidx.annotation:annotation": { "shasums": { - "jar": "d38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692" + "jar": "9029262bddce116e6d02be499e4afdba21f24c239087b76b3b57d7e98b490a36" }, - "version": "1.1.0" + "version": "1.2.0" }, "androidx.annotation:annotation-experimental:aar": { "shasums": { - "jar": "b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11" + "jar": "0157de61a2064047896a058080f3fd67ba57ad9a94857b3f7a363660243e3f90" }, - "version": "1.0.0" + "version": "1.1.0" }, "androidx.appcompat:appcompat-resources:aar": { "shasums": { - "jar": "c470297c03ff3de1c3d15dacf0be0cae63abc10b52f021dd07ae28daa3100fe5" + "jar": "e3306cd3e9a19a28a5de5ec3b379580f237c4d81c15c3d795404be9291890a75" }, - "version": "1.2.0" + "version": "1.3.1" }, "androidx.appcompat:appcompat:aar": { "shasums": { - "jar": "3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70" + "jar": "959b1daefe40d5e7eed1022f97730b22bc5fd52e6a6083eba284fa86c2971303" }, - "version": "1.2.0" + "version": "1.3.1" }, "androidx.arch.core:core-common": { "shasums": { @@ -64,6 +83,12 @@ }, "version": "2.1.0" }, + "androidx.autofill:autofill:aar": { + "shasums": { + "jar": "c9468f56e05006ea151a426c54957cd0799b8b83a579d2846dd22061f33e5ecd" + }, + "version": "1.0.0" + }, "androidx.cardview:cardview:aar": { "shasums": { "jar": "1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7" @@ -76,6 +101,120 @@ }, "version": "1.1.0" }, + "androidx.compose.animation:animation-core:aar": { + "shasums": { + "jar": "797048e2d03a9b1b98443fbe118d70f0955988e3fca56a71985ee3550be46f4e" + }, + "version": "1.1.1" + }, + "androidx.compose.animation:animation:aar": { + "shasums": { + "jar": "95fca9d5bbb8da8c4f351331558e7b2f4ec04db0cf290b021852423461d76a9c" + }, + "version": "1.1.1" + }, + "androidx.compose.compiler:compiler": { + "shasums": { + "jar": "925acb226edfade11905827a8387bca83f83d6bd1a13f48708d1efab2129520a" + }, + "version": "1.1.1" + }, + "androidx.compose.foundation:foundation-layout:aar": { + "shasums": { + "jar": "8337856c1babb54bdadfdb97eea47eb94640fc5557f91de47d35e9158258a971" + }, + "version": "1.1.1" + }, + "androidx.compose.foundation:foundation:aar": { + "shasums": { + "jar": "7f5fff6b1d462c7e411533b75ec0a9a73027f9a38f1e85ae8309295811ab64a0" + }, + "version": "1.1.1" + }, + "androidx.compose.material:material-icons-core:aar": { + "shasums": { + "jar": "8c9c1a9688a9f4dee57d31de8784d0970919fa939a48a466380815b9ebd84cfa" + }, + "version": "1.1.1" + }, + "androidx.compose.material:material-ripple:aar": { + "shasums": { + "jar": "c84f77c84e0c9ac17d513d38611894558bea6592c5318976fc8b0a51bc3cf8f2" + }, + "version": "1.1.1" + }, + "androidx.compose.material:material:aar": { + "shasums": { + "jar": "538ab37092d8e837e0d7e58bd854296e54033f449435db3baf913cc5ba4f97f3" + }, + "version": "1.1.1" + }, + "androidx.compose.runtime:runtime-saveable:aar": { + "shasums": { + "jar": "15b542e07bea14065336da0b255650433da46e250a160feaf39b90f2edc8b230" + }, + "version": "1.1.1" + }, + "androidx.compose.runtime:runtime:aar": { + "shasums": { + "jar": "e075b3e3952cb3775e7acc7ccdb8f2de777da76bd6d2ea2cc1f96d6f57b37763" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui-geometry:aar": { + "shasums": { + "jar": "4f1e34ae515af8c04e783275dded50bc8d156135fc35397fb1c81a9e01f173b3" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui-graphics:aar": { + "shasums": { + "jar": "e1719d8db7545e7f3be3baf42ac1865fb1cca36525d9b889601e2c2f4ea3aafc" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui-test-junit4:aar": { + "shasums": { + "jar": "db527d645365f76ab135a7b3a5225ea3a3a3171ded8d6092c732c2af033c9e4e" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui-test:aar": { + "shasums": { + "jar": "b710a1fe38c8acf9dbc7d678b45bdda61867013f2cad031b82547d0dc0b18e26" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui-text:aar": { + "shasums": { + "jar": "e4af67f79b658862e4d7437f243033ed64334fbbff3247d4df39785a1c2c75d4" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui-unit:aar": { + "shasums": { + "jar": "35b1d8f7f460874dde09fd7e630cd44052913695298076fc62de91a4fe1345a7" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui-util:aar": { + "shasums": { + "jar": "56420281f21e2888145ec9438ca4a632012792013b667da9aa03ea733e482e96" + }, + "version": "1.1.1" + }, + "androidx.compose.ui:ui:aar": { + "shasums": { + "jar": "f0243d6a2176c9bcb6f077e1212cbc890943391ec9a6e32a4f249ab7c2c807a6" + }, + "version": "1.1.1" + }, + "androidx.concurrent:concurrent-futures": { + "shasums": { + "jar": "5595a40e278a7b39fa78a09490e3d7f3faa95c7b01447148bd38b5ade0605c35" + }, + "version": "1.0.0" + }, "androidx.constraintlayout:constraintlayout-solver": { "shasums": { "jar": "b23732edbb3511d937fea1ffef047b0e6c001b50c1921f0d959fc384d706ec6a" @@ -96,15 +235,15 @@ }, "androidx.core:core-ktx:aar": { "shasums": { - "jar": "a151b7e21acc3d272e1d397a2084e76ccce88e8542adcc4e0cf1e0655063255f" + "jar": "070cc5f8864f449128a2f4b25ca5b67aa3adca3ee1bd611e2eaf1a18fad83178" }, - "version": "1.0.1" + "version": "1.1.0" }, "androidx.core:core:aar": { "shasums": { - "jar": "e92ea65a37d589943d405a6a54d1be9d12a225948f26c4e41e511dd55e81efb6" + "jar": "aaf6734226fff923784f92f65d78a2984dbf17534138855c5ce2038f18656e0b" }, - "version": "1.3.1" + "version": "1.7.0" }, "androidx.cursoradapter:cursoradapter:aar": { "shasums": { @@ -174,9 +313,9 @@ }, "androidx.fragment:fragment:aar": { "shasums": { - "jar": "fdd0eac80c6b26c79093a63fc699303f928cc1fa73ca7196d5590a77eb6d6873" + "jar": "12f0831b4f08092d5dda272c1923c11a022ff20ceffed3e801751e21bb8d1c1e" }, - "version": "1.2.0" + "version": "1.3.6" }, "androidx.interpolator:interpolator:aar": { "shasums": { @@ -192,9 +331,15 @@ }, "androidx.lifecycle:lifecycle-common": { "shasums": { - "jar": "63898dabf7cfe5ec5d7ed8b8c2564c1427be876e1496ead95c2703cf59d3734b" + "jar": "15848fb56db32f4c7cdc72b324003183d52a4884d6bf09be708ac7f587d139b5" }, - "version": "2.2.0" + "version": "2.3.1" + }, + "androidx.lifecycle:lifecycle-common-java8": { + "shasums": { + "jar": "a1ec63c1bb973443cb731d78ec336c5e20e7ee35c89cbb32d36f92c55bb02542" + }, + "version": "2.3.0" }, "androidx.lifecycle:lifecycle-extensions:aar": { "shasums": { @@ -210,9 +355,9 @@ }, "androidx.lifecycle:lifecycle-livedata-core:aar": { "shasums": { - "jar": "556c1f3af90aa9d7d0d330565adbf6da71b2429148bac91e07c485f4f9abf614" + "jar": "e55d38c372460f0a03997ddc950c67227511340fd74f8634d99d29653cd81ab1" }, - "version": "2.2.0" + "version": "2.3.1" }, "androidx.lifecycle:lifecycle-livedata-ktx:aar": { "shasums": { @@ -232,11 +377,17 @@ }, "version": "2.2.0" }, + "androidx.lifecycle:lifecycle-runtime-ktx:aar": { + "shasums": { + "jar": "7ad2987dd7f4075c0871a72cf07e9649d9cd790fc23dfab1972eca4710373873" + }, + "version": "2.3.1" + }, "androidx.lifecycle:lifecycle-runtime:aar": { "shasums": { - "jar": "2f866c07a1f33a8c9bb69a9545d4f20b4f0628cd0a155432386d7cb081e1e0bc" + "jar": "dd294f4a689c71ff877fd41f3b67a3a62f7760d44ce420e6130f1fc3569d8f00" }, - "version": "2.2.0" + "version": "2.3.1" }, "androidx.lifecycle:lifecycle-service:aar": { "shasums": { @@ -246,21 +397,21 @@ }, "androidx.lifecycle:lifecycle-viewmodel-ktx:aar": { "shasums": { - "jar": "f791001f2211947e56ad3d96d12c9ae93fc5589b88f08603f69a2265c9a7d702" + "jar": "5fb3591b6a54eeb3e204be0125d48eb987b8ea45a5048140036865482ccf9de9" }, - "version": "2.2.0" + "version": "2.3.1" }, "androidx.lifecycle:lifecycle-viewmodel-savedstate:aar": { "shasums": { - "jar": "f4cceafbf86acfc7f3ba6a61d6dc6842a6738c1274610767d3ab8f8a114cba97" + "jar": "97137a8af6a31776a14e4866ab808c7c0a791b484bdbc788bbd83e66407564c0" }, - "version": "1.0.0" + "version": "2.3.1" }, "androidx.lifecycle:lifecycle-viewmodel:aar": { "shasums": { - "jar": "967efab24d6c49dd414a8c0ac4a1cd09b018f0b8bb43b739ad360c4158ebde27" + "jar": "b6db4c274a12ff85a4747e1e6669c7e98aefa2571ace9d1f1a6fa6be417ce838" }, - "version": "2.2.0" + "version": "2.3.1" }, "androidx.loader:loader:aar": { "shasums": { @@ -340,6 +491,12 @@ }, "version": "1.0.0" }, + "androidx.profileinstaller:profileinstaller:aar": { + "shasums": { + "jar": "d85f562bc70f33595b46a893c22d64f5b4f856c19a02b1eb09aad00c3a2124ee" + }, + "version": "1.1.0" + }, "androidx.recyclerview:recyclerview:aar": { "shasums": { "jar": "f0d2b5a67d0a91ee1b1c73ef2b636a81f3563925ddd15a1d4e1c41ec28de7a4f" @@ -358,11 +515,17 @@ }, "version": "2.2.5" }, + "androidx.savedstate:savedstate-ktx:aar": { + "shasums": { + "jar": "e44d61347463b0fafeeb649cbcc3d7109b2eb5e11d1522e986105170cdebbf68" + }, + "version": "1.1.0" + }, "androidx.savedstate:savedstate:aar": { "shasums": { - "jar": "2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83" + "jar": "d60bbe44c2c08083a17c5dc678a6d6b4d0a2d664858016ab5c049cbea90a63b7" }, - "version": "1.0.0" + "version": "1.1.0" }, "androidx.sqlite:sqlite-framework:aar": { "shasums": { @@ -376,6 +539,12 @@ }, "version": "2.1.0" }, + "androidx.startup:startup-runtime:aar": { + "shasums": { + "jar": "ff081d2db7dd28aec59f74934c514fbaf4ae5aac5258495fe10d612a3622f876" + }, + "version": "1.0.0" + }, "androidx.test.espresso:espresso-accessibility:aar": { "shasums": { "jar": "e2ee8b50081c0b578521d112808321f737f3bf1acce035fe7be0e26ef00b491f" @@ -390,15 +559,15 @@ }, "androidx.test.espresso:espresso-core:aar": { "shasums": { - "jar": "beb4712c2520c1da30ac1f25506871f16ea5b83ee686ece5a258769df1a01e15" + "jar": "23ebf6014645e0c60aec7d1ed924d4d4c848ae8c3673b7d8d06b2ec6a56cafee" }, - "version": "3.2.0" + "version": "3.3.0" }, "androidx.test.espresso:espresso-idling-resource:aar": { "shasums": { - "jar": "c1a0454fe95788122ba652c3ecff7ec538c7e27de206aed970f2809fb8090d09" + "jar": "29519b112731f289cc6e2f9b2eccc5ea72c754b04272bb93370f45d7e170a7c6" }, - "version": "3.2.0" + "version": "3.3.0" }, "androidx.test.espresso:espresso-intents:aar": { "shasums": { @@ -408,9 +577,9 @@ }, "androidx.test.ext:junit:aar": { "shasums": { - "jar": "449df418d2916a0f86fe7dafb1edb09480fafb6e995d5c751c7d0d1970d4ae72" + "jar": "6c6ab120c640bf16fcaae69cb83c144d0ed6b6298562be0ac35e37ed969c0409" }, - "version": "1.1.1" + "version": "1.1.2" }, "androidx.test.ext:truth:aar": { "shasums": { @@ -424,6 +593,12 @@ }, "version": "2.2.0" }, + "androidx.test:annotation:aar": { + "shasums": { + "jar": "c0754928effe1968c3a9a7b55d1dfc7ceb1e1e7c9f3f09f98afd42431f712492" + }, + "version": "1.0.0" + }, "androidx.test:core:aar": { "shasums": { "jar": "671284e62e393f16ceae1a99a3a9a07bf1aacda29f8fe7b6b884355ef34c09cf" @@ -432,9 +607,9 @@ }, "androidx.test:monitor:aar": { "shasums": { - "jar": "46a912a1e175f27a97521af3f50e5af87c22c49275dd2c57c043740012806325" + "jar": "10b1723c436beecb5884c69f8473504bc59611f9463ae549c48b3cf8e73b09c0" }, - "version": "1.4.0" + "version": "1.5.0" }, "androidx.test:rules:aar": { "shasums": { @@ -444,9 +619,15 @@ }, "androidx.test:runner:aar": { "shasums": { - "jar": "5387e011167a3c8da08d99b5d59248c0e2da839317b48ebf202e31dc1f791ec1" + "jar": "61d13f5a9fcbbd73ba18fa84e1d6a0111c6e1c665a89b418126966e61fffd93b" }, - "version": "1.2.0" + "version": "1.3.0" + }, + "androidx.tracing:tracing:aar": { + "shasums": { + "jar": "07b8b6139665b884a162eccf97891ca50f7f56831233bf25168ae04f7b568612" + }, + "version": "1.0.0" }, "androidx.transition:transition:aar": { "shasums": { @@ -468,9 +649,9 @@ }, "androidx.versionedparcelable:versionedparcelable:aar": { "shasums": { - "jar": "9a1d77140ac222b7866b5054ee7d159bc1800987ed2d46dd6afdd145abb710c1" + "jar": "57e8d93260d18d5b9007c9eed3c64ad159de90c8609ebfc74a347cbd514535a4" }, - "version": "1.1.0" + "version": "1.1.1" }, "androidx.viewpager2:viewpager2:aar": { "shasums": { @@ -1758,13 +1939,31 @@ } }, "dependencies": { + "androidx.activity:activity-compose:aar": [ + "androidx.activity:activity-ktx:aar", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui:aar", + "org.jetbrains.kotlin:kotlin-stdlib" + ], + "androidx.activity:activity-ktx:aar": [ + "androidx.activity:activity:aar", + "androidx.core:core-ktx:aar", + "androidx.lifecycle:lifecycle-runtime-ktx:aar", + "androidx.lifecycle:lifecycle-viewmodel-ktx:aar", + "androidx.savedstate:savedstate-ktx:aar", + "org.jetbrains.kotlin:kotlin-stdlib" + ], "androidx.activity:activity:aar": [ "androidx.annotation:annotation", + "androidx.collection:collection", "androidx.core:core:aar", "androidx.lifecycle:lifecycle-runtime:aar", "androidx.lifecycle:lifecycle-viewmodel-savedstate:aar", "androidx.lifecycle:lifecycle-viewmodel:aar", - "androidx.savedstate:savedstate:aar" + "androidx.savedstate:savedstate:aar", + "androidx.tracing:tracing:aar", + "org.jetbrains.kotlin:kotlin-stdlib" ], "androidx.appcompat:appcompat-resources:aar": [ "androidx.annotation:annotation", @@ -1774,13 +1973,17 @@ "androidx.vectordrawable:vectordrawable:aar" ], "androidx.appcompat:appcompat:aar": [ + "androidx.activity:activity:aar", "androidx.annotation:annotation", "androidx.appcompat:appcompat-resources:aar", "androidx.collection:collection", "androidx.core:core:aar", "androidx.cursoradapter:cursoradapter:aar", "androidx.drawerlayout:drawerlayout:aar", - "androidx.fragment:fragment:aar" + "androidx.fragment:fragment:aar", + "androidx.lifecycle:lifecycle-runtime:aar", + "androidx.lifecycle:lifecycle-viewmodel:aar", + "androidx.savedstate:savedstate:aar" ], "androidx.arch.core:core-common": [ "androidx.annotation:annotation" @@ -1795,12 +1998,184 @@ "junit:junit", "org.mockito:mockito-core" ], + "androidx.autofill:autofill:aar": [ + "androidx.core:core:aar" + ], "androidx.cardview:cardview:aar": [ "androidx.annotation:annotation" ], "androidx.collection:collection": [ "androidx.annotation:annotation" ], + "androidx.compose.animation:animation-core:aar": [ + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlinx:kotlinx-coroutines-core" + ], + "androidx.compose.animation:animation:aar": [ + "androidx.annotation:annotation", + "androidx.compose.animation:animation-core:aar", + "androidx.compose.foundation:foundation-layout:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-geometry:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "org.jetbrains.kotlin:kotlin-stdlib-common" + ], + "androidx.compose.foundation:foundation-layout:aar": [ + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "org.jetbrains.kotlin:kotlin-stdlib-common" + ], + "androidx.compose.foundation:foundation:aar": [ + "androidx.annotation:annotation", + "androidx.compose.animation:animation:aar", + "androidx.compose.foundation:foundation-layout:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "org.jetbrains.kotlin:kotlin-stdlib-common" + ], + "androidx.compose.material:material-icons-core:aar": [ + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui:aar", + "org.jetbrains.kotlin:kotlin-stdlib" + ], + "androidx.compose.material:material-ripple:aar": [ + "androidx.compose.animation:animation:aar", + "androidx.compose.foundation:foundation:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-util:aar", + "org.jetbrains.kotlin:kotlin-stdlib-common" + ], + "androidx.compose.material:material:aar": [ + "androidx.compose.animation:animation-core:aar", + "androidx.compose.animation:animation:aar", + "androidx.compose.foundation:foundation-layout:aar", + "androidx.compose.foundation:foundation:aar", + "androidx.compose.material:material-icons-core:aar", + "androidx.compose.material:material-ripple:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "androidx.lifecycle:lifecycle-runtime:aar", + "androidx.lifecycle:lifecycle-viewmodel:aar", + "androidx.savedstate:savedstate:aar", + "org.jetbrains.kotlin:kotlin-stdlib-common" + ], + "androidx.compose.runtime:runtime-saveable:aar": [ + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime:aar", + "org.jetbrains.kotlin:kotlin-stdlib" + ], + "androidx.compose.runtime:runtime:aar": [ + "androidx.annotation:annotation", + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlinx:kotlinx-coroutines-android" + ], + "androidx.compose.ui:ui-geometry:aar": [ + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-util:aar", + "org.jetbrains.kotlin:kotlin-stdlib" + ], + "androidx.compose.ui:ui-graphics:aar": [ + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "org.jetbrains.kotlin:kotlin-stdlib-common" + ], + "androidx.compose.ui:ui-test-junit4:aar": [ + "androidx.activity:activity-compose:aar", + "androidx.activity:activity:aar", + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.ui:ui-test:aar", + "androidx.lifecycle:lifecycle-common", + "androidx.lifecycle:lifecycle-runtime:aar", + "androidx.test.espresso:espresso-core:aar", + "androidx.test.espresso:espresso-idling-resource:aar", + "androidx.test.ext:junit:aar", + "androidx.test:core:aar", + "androidx.test:monitor:aar", + "junit:junit", + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlin:kotlin-stdlib-common", + "org.jetbrains.kotlinx:kotlinx-coroutines-core", + "org.jetbrains.kotlinx:kotlinx-coroutines-test" + ], + "androidx.compose.ui:ui-test:aar": [ + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "androidx.test.espresso:espresso-core:aar", + "androidx.test:monitor:aar", + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlin:kotlin-stdlib-common", + "org.jetbrains.kotlinx:kotlinx-coroutines-core", + "org.jetbrains.kotlinx:kotlinx-coroutines-test" + ], + "androidx.compose.ui:ui-text:aar": [ + "androidx.annotation:annotation", + "androidx.collection:collection", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.core:core:aar", + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlin:kotlin-stdlib-common" + ], + "androidx.compose.ui:ui-unit:aar": [ + "androidx.annotation:annotation", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-geometry:aar", + "androidx.compose.ui:ui-util:aar", + "org.jetbrains.kotlin:kotlin-stdlib" + ], + "androidx.compose.ui:ui-util:aar": [ + "org.jetbrains.kotlin:kotlin-stdlib" + ], + "androidx.compose.ui:ui:aar": [ + "androidx.annotation:annotation", + "androidx.autofill:autofill:aar", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-geometry:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.lifecycle:lifecycle-common-java8", + "androidx.lifecycle:lifecycle-runtime:aar", + "androidx.lifecycle:lifecycle-viewmodel:aar", + "androidx.profileinstaller:profileinstaller:aar", + "androidx.savedstate:savedstate:aar", + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlin:kotlin-stdlib-common", + "org.jetbrains.kotlinx:kotlinx-coroutines-android", + "org.jetbrains.kotlinx:kotlinx-coroutines-core" + ], + "androidx.concurrent:concurrent-futures": [ + "androidx.annotation:annotation", + "com.google.guava:listenablefuture" + ], "androidx.constraintlayout:constraintlayout:aar": [ "androidx.appcompat:appcompat:aar", "androidx.constraintlayout:constraintlayout-solver", @@ -1819,7 +2194,9 @@ ], "androidx.core:core:aar": [ "androidx.annotation:annotation", + "androidx.annotation:annotation-experimental:aar", "androidx.collection:collection", + "androidx.concurrent:concurrent-futures", "androidx.lifecycle:lifecycle-runtime:aar", "androidx.versionedparcelable:versionedparcelable:aar" ], @@ -1881,12 +2258,14 @@ "androidx.fragment:fragment:aar": [ "androidx.activity:activity:aar", "androidx.annotation:annotation", + "androidx.annotation:annotation-experimental:aar", "androidx.collection:collection", "androidx.core:core:aar", "androidx.lifecycle:lifecycle-livedata-core:aar", "androidx.lifecycle:lifecycle-viewmodel-savedstate:aar", "androidx.lifecycle:lifecycle-viewmodel:aar", "androidx.loader:loader:aar", + "androidx.savedstate:savedstate:aar", "androidx.viewpager:viewpager:aar" ], "androidx.interpolator:interpolator:aar": [ @@ -1903,6 +2282,10 @@ "androidx.lifecycle:lifecycle-common": [ "androidx.annotation:annotation" ], + "androidx.lifecycle:lifecycle-common-java8": [ + "androidx.annotation:annotation", + "androidx.lifecycle:lifecycle-common" + ], "androidx.lifecycle:lifecycle-extensions:aar": [ "androidx.arch.core:core-common", "androidx.arch.core:core-runtime:aar", @@ -1937,9 +2320,16 @@ "androidx.lifecycle:lifecycle-process:aar": [ "androidx.lifecycle:lifecycle-runtime:aar" ], + "androidx.lifecycle:lifecycle-runtime-ktx:aar": [ + "androidx.annotation:annotation", + "androidx.lifecycle:lifecycle-runtime:aar", + "org.jetbrains.kotlin:kotlin-stdlib", + "org.jetbrains.kotlinx:kotlinx-coroutines-android" + ], "androidx.lifecycle:lifecycle-runtime:aar": [ "androidx.annotation:annotation", "androidx.arch.core:core-common", + "androidx.arch.core:core-runtime:aar", "androidx.lifecycle:lifecycle-common" ], "androidx.lifecycle:lifecycle-service:aar": [ @@ -2010,6 +2400,10 @@ "androidx.print:print:aar": [ "androidx.annotation:annotation" ], + "androidx.profileinstaller:profileinstaller:aar": [ + "androidx.annotation:annotation", + "androidx.startup:startup-runtime:aar" + ], "androidx.recyclerview:recyclerview:aar": [ "androidx.annotation:annotation", "androidx.collection:collection", @@ -2025,6 +2419,10 @@ "androidx.sqlite:sqlite-framework:aar", "androidx.sqlite:sqlite:aar" ], + "androidx.savedstate:savedstate-ktx:aar": [ + "androidx.savedstate:savedstate:aar", + "org.jetbrains.kotlin:kotlin-stdlib" + ], "androidx.savedstate:savedstate:aar": [ "androidx.annotation:annotation", "androidx.arch.core:core-common", @@ -2037,6 +2435,10 @@ "androidx.sqlite:sqlite:aar": [ "androidx.annotation:annotation" ], + "androidx.startup:startup-runtime:aar": [ + "androidx.annotation:annotation", + "androidx.tracing:tracing:aar" + ], "androidx.test.espresso:espresso-accessibility:aar": [ "androidx.test.espresso:espresso-core:aar", "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework" @@ -2075,13 +2477,18 @@ "com.google.guava:guava", "com.google.truth:truth" ], + "androidx.test:annotation:aar": [ + "androidx.annotation:annotation", + "androidx.annotation:annotation-experimental:aar" + ], "androidx.test:core:aar": [ "androidx.annotation:annotation", "androidx.lifecycle:lifecycle-common", "androidx.test:monitor:aar" ], "androidx.test:monitor:aar": [ - "androidx.annotation:annotation" + "androidx.annotation:annotation", + "androidx.test:annotation:aar" ], "androidx.test:rules:aar": [ "androidx.test:runner:aar" @@ -2089,8 +2496,10 @@ "androidx.test:runner:aar": [ "androidx.annotation:annotation", "androidx.test:monitor:aar", - "junit:junit", - "net.sf.kxml:kxml2" + "junit:junit" + ], + "androidx.tracing:tracing:aar": [ + "androidx.annotation:annotation" ], "androidx.transition:transition:aar": [ "androidx.annotation:annotation", @@ -2996,6 +3405,15 @@ "androidx.collection:collection": [ "androidx.collection" ], + "androidx.compose.compiler:compiler": [ + "androidx.compose.compiler.plugins.kotlin", + "androidx.compose.compiler.plugins.kotlin.analysis", + "androidx.compose.compiler.plugins.kotlin.lower", + "androidx.compose.compiler.plugins.kotlin.lower.decoys" + ], + "androidx.concurrent:concurrent-futures": [ + "androidx.concurrent.futures" + ], "androidx.constraintlayout:constraintlayout-solver": [ "androidx.constraintlayout.solver", "androidx.constraintlayout.solver.state", @@ -3031,6 +3449,9 @@ "androidx.lifecycle:lifecycle-common": [ "androidx.lifecycle" ], + "androidx.lifecycle:lifecycle-common-java8": [ + "androidx.lifecycle" + ], "androidx.room:room-common": [ "androidx.room" ], @@ -6372,6 +6793,8 @@ }, "repositories": { "https://maven.google.com/": [ + "androidx.activity:activity-compose:aar", + "androidx.activity:activity-ktx:aar", "androidx.activity:activity:aar", "androidx.annotation:annotation", "androidx.annotation:annotation-experimental:aar", @@ -6380,8 +6803,28 @@ "androidx.arch.core:core-common", "androidx.arch.core:core-runtime:aar", "androidx.arch.core:core-testing:aar", + "androidx.autofill:autofill:aar", "androidx.cardview:cardview:aar", "androidx.collection:collection", + "androidx.compose.animation:animation-core:aar", + "androidx.compose.animation:animation:aar", + "androidx.compose.compiler:compiler", + "androidx.compose.foundation:foundation-layout:aar", + "androidx.compose.foundation:foundation:aar", + "androidx.compose.material:material-icons-core:aar", + "androidx.compose.material:material-ripple:aar", + "androidx.compose.material:material:aar", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-geometry:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-test-junit4:aar", + "androidx.compose.ui:ui-test:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "androidx.concurrent:concurrent-futures", "androidx.constraintlayout:constraintlayout-solver", "androidx.constraintlayout:constraintlayout:aar", "androidx.coordinatorlayout:coordinatorlayout:aar", @@ -6402,12 +6845,14 @@ "androidx.interpolator:interpolator:aar", "androidx.legacy:legacy-support-core-utils:aar", "androidx.lifecycle:lifecycle-common", + "androidx.lifecycle:lifecycle-common-java8", "androidx.lifecycle:lifecycle-extensions:aar", "androidx.lifecycle:lifecycle-livedata-core-ktx:aar", "androidx.lifecycle:lifecycle-livedata-core:aar", "androidx.lifecycle:lifecycle-livedata-ktx:aar", "androidx.lifecycle:lifecycle-livedata:aar", "androidx.lifecycle:lifecycle-process:aar", + "androidx.lifecycle:lifecycle-runtime-ktx:aar", "androidx.lifecycle:lifecycle-runtime:aar", "androidx.lifecycle:lifecycle-service:aar", "androidx.lifecycle:lifecycle-viewmodel-ktx:aar", @@ -6426,12 +6871,15 @@ "androidx.navigation:navigation-ui-ktx:aar", "androidx.navigation:navigation-ui:aar", "androidx.print:print:aar", + "androidx.profileinstaller:profileinstaller:aar", "androidx.recyclerview:recyclerview:aar", "androidx.room:room-common", "androidx.room:room-runtime:aar", + "androidx.savedstate:savedstate-ktx:aar", "androidx.savedstate:savedstate:aar", "androidx.sqlite:sqlite-framework:aar", "androidx.sqlite:sqlite:aar", + "androidx.startup:startup-runtime:aar", "androidx.test.espresso:espresso-accessibility:aar", "androidx.test.espresso:espresso-contrib:aar", "androidx.test.espresso:espresso-core:aar", @@ -6440,10 +6888,12 @@ "androidx.test.ext:junit:aar", "androidx.test.ext:truth:aar", "androidx.test.uiautomator:uiautomator:aar", + "androidx.test:annotation:aar", "androidx.test:core:aar", "androidx.test:monitor:aar", "androidx.test:rules:aar", "androidx.test:runner:aar", + "androidx.tracing:tracing:aar", "androidx.transition:transition:aar", "androidx.vectordrawable:vectordrawable-animated:aar", "androidx.vectordrawable:vectordrawable:aar", @@ -6664,6 +7114,8 @@ "xml-apis:xml-apis" ], "https://repo1.maven.org/maven2/": [ + "androidx.activity:activity-compose:aar", + "androidx.activity:activity-ktx:aar", "androidx.activity:activity:aar", "androidx.annotation:annotation", "androidx.annotation:annotation-experimental:aar", @@ -6672,8 +7124,28 @@ "androidx.arch.core:core-common", "androidx.arch.core:core-runtime:aar", "androidx.arch.core:core-testing:aar", + "androidx.autofill:autofill:aar", "androidx.cardview:cardview:aar", "androidx.collection:collection", + "androidx.compose.animation:animation-core:aar", + "androidx.compose.animation:animation:aar", + "androidx.compose.compiler:compiler", + "androidx.compose.foundation:foundation-layout:aar", + "androidx.compose.foundation:foundation:aar", + "androidx.compose.material:material-icons-core:aar", + "androidx.compose.material:material-ripple:aar", + "androidx.compose.material:material:aar", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-geometry:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-test-junit4:aar", + "androidx.compose.ui:ui-test:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "androidx.concurrent:concurrent-futures", "androidx.constraintlayout:constraintlayout-solver", "androidx.constraintlayout:constraintlayout:aar", "androidx.coordinatorlayout:coordinatorlayout:aar", @@ -6694,12 +7166,14 @@ "androidx.interpolator:interpolator:aar", "androidx.legacy:legacy-support-core-utils:aar", "androidx.lifecycle:lifecycle-common", + "androidx.lifecycle:lifecycle-common-java8", "androidx.lifecycle:lifecycle-extensions:aar", "androidx.lifecycle:lifecycle-livedata-core-ktx:aar", "androidx.lifecycle:lifecycle-livedata-core:aar", "androidx.lifecycle:lifecycle-livedata-ktx:aar", "androidx.lifecycle:lifecycle-livedata:aar", "androidx.lifecycle:lifecycle-process:aar", + "androidx.lifecycle:lifecycle-runtime-ktx:aar", "androidx.lifecycle:lifecycle-runtime:aar", "androidx.lifecycle:lifecycle-service:aar", "androidx.lifecycle:lifecycle-viewmodel-ktx:aar", @@ -6718,12 +7192,15 @@ "androidx.navigation:navigation-ui-ktx:aar", "androidx.navigation:navigation-ui:aar", "androidx.print:print:aar", + "androidx.profileinstaller:profileinstaller:aar", "androidx.recyclerview:recyclerview:aar", "androidx.room:room-common", "androidx.room:room-runtime:aar", + "androidx.savedstate:savedstate-ktx:aar", "androidx.savedstate:savedstate:aar", "androidx.sqlite:sqlite-framework:aar", "androidx.sqlite:sqlite:aar", + "androidx.startup:startup-runtime:aar", "androidx.test.espresso:espresso-accessibility:aar", "androidx.test.espresso:espresso-contrib:aar", "androidx.test.espresso:espresso-core:aar", @@ -6732,10 +7209,12 @@ "androidx.test.ext:junit:aar", "androidx.test.ext:truth:aar", "androidx.test.uiautomator:uiautomator:aar", + "androidx.test:annotation:aar", "androidx.test:core:aar", "androidx.test:monitor:aar", "androidx.test:rules:aar", "androidx.test:runner:aar", + "androidx.tracing:tracing:aar", "androidx.transition:transition:aar", "androidx.vectordrawable:vectordrawable-animated:aar", "androidx.vectordrawable:vectordrawable:aar", @@ -6956,6 +7435,8 @@ "xml-apis:xml-apis" ], "https://oss.sonatype.org/content/repositories/snapshots/": [ + "androidx.activity:activity-compose:aar", + "androidx.activity:activity-ktx:aar", "androidx.activity:activity:aar", "androidx.annotation:annotation", "androidx.annotation:annotation-experimental:aar", @@ -6964,8 +7445,28 @@ "androidx.arch.core:core-common", "androidx.arch.core:core-runtime:aar", "androidx.arch.core:core-testing:aar", + "androidx.autofill:autofill:aar", "androidx.cardview:cardview:aar", "androidx.collection:collection", + "androidx.compose.animation:animation-core:aar", + "androidx.compose.animation:animation:aar", + "androidx.compose.compiler:compiler", + "androidx.compose.foundation:foundation-layout:aar", + "androidx.compose.foundation:foundation:aar", + "androidx.compose.material:material-icons-core:aar", + "androidx.compose.material:material-ripple:aar", + "androidx.compose.material:material:aar", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-geometry:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-test-junit4:aar", + "androidx.compose.ui:ui-test:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "androidx.concurrent:concurrent-futures", "androidx.constraintlayout:constraintlayout-solver", "androidx.constraintlayout:constraintlayout:aar", "androidx.coordinatorlayout:coordinatorlayout:aar", @@ -6986,12 +7487,14 @@ "androidx.interpolator:interpolator:aar", "androidx.legacy:legacy-support-core-utils:aar", "androidx.lifecycle:lifecycle-common", + "androidx.lifecycle:lifecycle-common-java8", "androidx.lifecycle:lifecycle-extensions:aar", "androidx.lifecycle:lifecycle-livedata-core-ktx:aar", "androidx.lifecycle:lifecycle-livedata-core:aar", "androidx.lifecycle:lifecycle-livedata-ktx:aar", "androidx.lifecycle:lifecycle-livedata:aar", "androidx.lifecycle:lifecycle-process:aar", + "androidx.lifecycle:lifecycle-runtime-ktx:aar", "androidx.lifecycle:lifecycle-runtime:aar", "androidx.lifecycle:lifecycle-service:aar", "androidx.lifecycle:lifecycle-viewmodel-ktx:aar", @@ -7010,12 +7513,15 @@ "androidx.navigation:navigation-ui-ktx:aar", "androidx.navigation:navigation-ui:aar", "androidx.print:print:aar", + "androidx.profileinstaller:profileinstaller:aar", "androidx.recyclerview:recyclerview:aar", "androidx.room:room-common", "androidx.room:room-runtime:aar", + "androidx.savedstate:savedstate-ktx:aar", "androidx.savedstate:savedstate:aar", "androidx.sqlite:sqlite-framework:aar", "androidx.sqlite:sqlite:aar", + "androidx.startup:startup-runtime:aar", "androidx.test.espresso:espresso-accessibility:aar", "androidx.test.espresso:espresso-contrib:aar", "androidx.test.espresso:espresso-core:aar", @@ -7024,10 +7530,12 @@ "androidx.test.ext:junit:aar", "androidx.test.ext:truth:aar", "androidx.test.uiautomator:uiautomator:aar", + "androidx.test:annotation:aar", "androidx.test:core:aar", "androidx.test:monitor:aar", "androidx.test:rules:aar", "androidx.test:runner:aar", + "androidx.tracing:tracing:aar", "androidx.transition:transition:aar", "androidx.vectordrawable:vectordrawable-animated:aar", "androidx.vectordrawable:vectordrawable:aar", @@ -7248,6 +7756,8 @@ "xml-apis:xml-apis" ], "https://maven.fabric.io/public/": [ + "androidx.activity:activity-compose:aar", + "androidx.activity:activity-ktx:aar", "androidx.activity:activity:aar", "androidx.annotation:annotation", "androidx.annotation:annotation-experimental:aar", @@ -7256,8 +7766,28 @@ "androidx.arch.core:core-common", "androidx.arch.core:core-runtime:aar", "androidx.arch.core:core-testing:aar", + "androidx.autofill:autofill:aar", "androidx.cardview:cardview:aar", "androidx.collection:collection", + "androidx.compose.animation:animation-core:aar", + "androidx.compose.animation:animation:aar", + "androidx.compose.compiler:compiler", + "androidx.compose.foundation:foundation-layout:aar", + "androidx.compose.foundation:foundation:aar", + "androidx.compose.material:material-icons-core:aar", + "androidx.compose.material:material-ripple:aar", + "androidx.compose.material:material:aar", + "androidx.compose.runtime:runtime-saveable:aar", + "androidx.compose.runtime:runtime:aar", + "androidx.compose.ui:ui-geometry:aar", + "androidx.compose.ui:ui-graphics:aar", + "androidx.compose.ui:ui-test-junit4:aar", + "androidx.compose.ui:ui-test:aar", + "androidx.compose.ui:ui-text:aar", + "androidx.compose.ui:ui-unit:aar", + "androidx.compose.ui:ui-util:aar", + "androidx.compose.ui:ui:aar", + "androidx.concurrent:concurrent-futures", "androidx.constraintlayout:constraintlayout-solver", "androidx.constraintlayout:constraintlayout:aar", "androidx.coordinatorlayout:coordinatorlayout:aar", @@ -7278,12 +7808,14 @@ "androidx.interpolator:interpolator:aar", "androidx.legacy:legacy-support-core-utils:aar", "androidx.lifecycle:lifecycle-common", + "androidx.lifecycle:lifecycle-common-java8", "androidx.lifecycle:lifecycle-extensions:aar", "androidx.lifecycle:lifecycle-livedata-core-ktx:aar", "androidx.lifecycle:lifecycle-livedata-core:aar", "androidx.lifecycle:lifecycle-livedata-ktx:aar", "androidx.lifecycle:lifecycle-livedata:aar", "androidx.lifecycle:lifecycle-process:aar", + "androidx.lifecycle:lifecycle-runtime-ktx:aar", "androidx.lifecycle:lifecycle-runtime:aar", "androidx.lifecycle:lifecycle-service:aar", "androidx.lifecycle:lifecycle-viewmodel-ktx:aar", @@ -7302,12 +7834,15 @@ "androidx.navigation:navigation-ui-ktx:aar", "androidx.navigation:navigation-ui:aar", "androidx.print:print:aar", + "androidx.profileinstaller:profileinstaller:aar", "androidx.recyclerview:recyclerview:aar", "androidx.room:room-common", "androidx.room:room-runtime:aar", + "androidx.savedstate:savedstate-ktx:aar", "androidx.savedstate:savedstate:aar", "androidx.sqlite:sqlite-framework:aar", "androidx.sqlite:sqlite:aar", + "androidx.startup:startup-runtime:aar", "androidx.test.espresso:espresso-accessibility:aar", "androidx.test.espresso:espresso-contrib:aar", "androidx.test.espresso:espresso-core:aar", @@ -7316,10 +7851,12 @@ "androidx.test.ext:junit:aar", "androidx.test.ext:truth:aar", "androidx.test.uiautomator:uiautomator:aar", + "androidx.test:annotation:aar", "androidx.test:core:aar", "androidx.test:monitor:aar", "androidx.test:rules:aar", "androidx.test:runner:aar", + "androidx.tracing:tracing:aar", "androidx.transition:transition:aar", "androidx.vectordrawable:vectordrawable-animated:aar", "androidx.vectordrawable:vectordrawable:aar", diff --git a/third_party/versions.bzl b/third_party/versions.bzl index f9e0864672d..2bcb6612f34 100644 --- a/third_party/versions.bzl +++ b/third_party/versions.bzl @@ -20,8 +20,15 @@ https://github.com/oppia/oppia-android/wiki/Updating-Maven-Dependencies # Note to developers: Please keep this dict sorted by key to make it easier to find dependencies. # This list should contain only production (non-test) dependencies. MAVEN_PRODUCTION_DEPENDENCY_VERSIONS = { + "androidx.activity:activity-compose": "1.4.0", "androidx.annotation:annotation": "1.1.0", - "androidx.appcompat:appcompat": "1.2.0", + "androidx.appcompat:appcompat": "1.3.1", + "androidx.compose.compiler:compiler": "1.1.1", + "androidx.compose.foundation:foundation": "1.1.1", + "androidx.compose.foundation:foundation-layout": "1.1.1", + "androidx.compose.material:material": "1.1.1", + "androidx.compose.runtime:runtime": "1.1.1", + "androidx.compose.ui:ui": "1.1.1", "androidx.constraintlayout:constraintlayout": "1.1.3", "androidx.core:core": "1.0.1", "androidx.core:core-ktx": "1.0.1", @@ -91,6 +98,7 @@ MAVEN_PRODUCTION_DEPENDENCY_VERSIONS = { # cannot be included in production builds of the app. MAVEN_TEST_DEPENDENCY_VERSIONS = { "androidx.arch.core:core-testing": "2.1.0", + "androidx.compose.ui:ui-test-junit4": "1.1.1", "androidx.test.espresso:espresso-accessibility": "3.1.0", "androidx.test.espresso:espresso-contrib": "3.1.0", "androidx.test.espresso:espresso-core": "3.2.0", diff --git a/tools/kotlin/BUILD.bazel b/tools/kotlin/BUILD.bazel index ad4704f6609..5908169ccf9 100644 --- a/tools/kotlin/BUILD.bazel +++ b/tools/kotlin/BUILD.bazel @@ -3,7 +3,7 @@ Configurations for a codebase-wide build toolchain for Kotlin. """ load("@bazel_skylib//rules:common_settings.bzl", "string_flag") -load("@io_bazel_rules_kotlin//kotlin:core.bzl", "define_kt_toolchain", "kt_javac_options", "kt_kotlinc_options") +load("@io_bazel_rules_kotlin//kotlin:core.bzl", "define_kt_toolchain", "kt_compiler_plugin", "kt_javac_options", "kt_kotlinc_options") # This exposes a patch that fixes an issue with duplicate processor arguments. See: # github.com/bazelbuild/rules_kotlin/pull/940. @@ -54,3 +54,11 @@ define_kt_toolchain( kotlinc_options = ":oppia_kotlinc_options", language_version = "1.6", ) + +kt_compiler_plugin( + name = "jetpack_compose_compiler_plugin", + id = "androidx.compose.compiler", + target_embedded_compiler = True, + visibility = ["//app:app_visibility"], + deps = ["//third_party:androidx_compose_compiler_compiler"], +) diff --git a/utility/build.gradle b/utility/build.gradle index 487c60f8b71..53e7905913f 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -4,10 +4,10 @@ apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 - buildToolsVersion "29.0.2" + buildToolsVersion "30.0.2" defaultConfig { - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" diff --git a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt index cb95c888227..ab091a7bba7 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt @@ -854,6 +854,7 @@ class EventBundleCreator @Inject constructor( ScreenName.UNRECOGNIZED -> "unrecognized" ScreenName.FOREGROUND_SCREEN -> "foreground_screen" ScreenName.SURVEY_ACTIVITY -> "survey_activity" + ScreenName.CLASSROOM_LIST_ACTIVITY -> "classroom_list_activity" } private fun AppLanguageSelection.toAnalyticsText(): String {