From b278b5e607544d2bdb4ce8645d61647ff5164e46 Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Sun, 30 Jun 2024 20:17:58 +0300
Subject: [PATCH 1/6] Fix Part of #4938: Introduce Onboarding profile type
screen (#5378)
## Explanation
Fixes Part of #4938: Introduce Onboarding profile type screen
Adds the profile type selection screen.
The new "I am a student" flow is not implemented, and tests have been
set up to fail when the flow has been implemented.
The "I am a parent/teacher" flow has been set up to route to the
existing profile screen.
## 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)).
All tests pass on Espresso
![Screenshot 2024-05-23 at 18 06
46](https://github.com/oppia/oppia-android/assets/59600948/6d2414c9-6ddf-4340-a408-52a7a5c8acdc)
## For UI-specific PRs only
||||
| --- | --- | --- |
|| Portrait | Landscape |
| Mobile Light Mode
|![Screenshot_1719762068](https://github.com/oppia/oppia-android/assets/59600948/6adbff2d-6cc7-4884-bd31-8ddbc4f0d359)|![Screenshot_1719762228](https://github.com/oppia/oppia-android/assets/59600948/e9dcb7bc-9d1b-44e7-86ea-2f4d669fe5c6)|
| Mobile Dark Mode
|![Screenshot_1719761957](https://github.com/oppia/oppia-android/assets/59600948/c1248dda-d377-4586-9613-803517fd7d24)|![Screenshot_1719762205](https://github.com/oppia/oppia-android/assets/59600948/9cf78a42-8d7d-442b-9b4e-8db02d3c5bdf)|
| Tablet Light Mode
|![Screenshot_1719762364](https://github.com/oppia/oppia-android/assets/59600948/ee8d2de4-90e6-478c-b9e2-cfbba5b7b012)|![Screenshot_1719762334](https://github.com/oppia/oppia-android/assets/59600948/6e5d6f81-b7e1-4ac4-ab99-05832bb7a7a8)|
| Tablet Dark Mode
|![Screenshot_1719762388](https://github.com/oppia/oppia-android/assets/59600948/1e74ded0-4110-4995-a8f8-68feba04e769)|![Screenshot_1719762309](https://github.com/oppia/oppia-android/assets/59600948/8edef377-64aa-4828-aba4-42804a7fb029)|
---
app/src/main/AndroidManifest.xml | 4 +
.../app/activity/ActivityComponentImpl.kt | 2 +
.../app/fragment/FragmentComponentImpl.kt | 2 +
.../onboarding/OnboardingFragmentPresenter.kt | 8 +
.../OnboardingProfileTypeActivity.kt | 32 ++
.../OnboardingProfileTypeActivityPresenter.kt | 41 ++
.../OnboardingProfileTypeFragment.kt | 29 +
.../OnboardingProfileTypeFragmentPresenter.kt | 41 ++
app/src/main/res/drawable/learner_otter.xml | 542 ++++++++++++++++++
...nboarding_back_button_white_background.xml | 5 +
.../res/drawable/parent_teacher_otter.xml | 414 +++++++++++++
.../onboarding_profile_type_fragment.xml | 124 ++++
.../onboarding_profile_type_fragment.xml | 138 +++++
.../onboarding_profile_type_fragment.xml | 138 +++++
.../onboarding_profile_type_activity.xml | 10 +
.../onboarding_profile_type_fragment.xml | 137 +++++
.../main/res/values-night/color_palette.xml | 1 +
app/src/main/res/values/color_defs.xml | 5 +-
app/src/main/res/values/color_palette.xml | 5 +-
app/src/main/res/values/component_colors.xml | 4 +
app/src/main/res/values/dimens.xml | 2 +
app/src/main/res/values/strings.xml | 11 +
app/src/main/res/values/styles.xml | 43 ++
.../OnboardingProfileTypeActivityTest.kt | 215 +++++++
.../OnboardingProfileTypeFragmentTest.kt | 398 +++++++++++++
model/src/main/proto/screens.proto | 3 +
.../file_content_validation_checks.textproto | 2 +
scripts/assets/test_file_exemptions.textproto | 8 +
.../util/logging/EventBundleCreator.kt | 1 +
29 files changed, 2363 insertions(+), 2 deletions(-)
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivity.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityPresenter.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragment.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
create mode 100644 app/src/main/res/drawable/learner_otter.xml
create mode 100644 app/src/main/res/drawable/onboarding_back_button_white_background.xml
create mode 100644 app/src/main/res/drawable/parent_teacher_otter.xml
create mode 100644 app/src/main/res/layout-land/onboarding_profile_type_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-land/onboarding_profile_type_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-port/onboarding_profile_type_fragment.xml
create mode 100644 app/src/main/res/layout/onboarding_profile_type_activity.xml
create mode 100644 app/src/main/res/layout/onboarding_profile_type_fragment.xml
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityTest.kt
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 235bb4a479f..b25b8ccfc86 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -336,6 +336,10 @@
android:name=".app.classroom.ClassroomListActivity"
android:label="@string/classroom_list_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/onboarding_back_button_white_background.xml b/app/src/main/res/drawable/onboarding_back_button_white_background.xml
new file mode 100644
index 00000000000..47b0bf7daa1
--- /dev/null
+++ b/app/src/main/res/drawable/onboarding_back_button_white_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/parent_teacher_otter.xml b/app/src/main/res/drawable/parent_teacher_otter.xml
new file mode 100644
index 00000000000..abeec4882c4
--- /dev/null
+++ b/app/src/main/res/drawable/parent_teacher_otter.xml
@@ -0,0 +1,414 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/onboarding_profile_type_fragment.xml b/app/src/main/res/layout-land/onboarding_profile_type_fragment.xml
new file mode 100644
index 00000000000..40df0c99b39
--- /dev/null
+++ b/app/src/main/res/layout-land/onboarding_profile_type_fragment.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-land/onboarding_profile_type_fragment.xml b/app/src/main/res/layout-sw600dp-land/onboarding_profile_type_fragment.xml
new file mode 100644
index 00000000000..7dd593a18db
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/onboarding_profile_type_fragment.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-port/onboarding_profile_type_fragment.xml b/app/src/main/res/layout-sw600dp-port/onboarding_profile_type_fragment.xml
new file mode 100644
index 00000000000..932631d6119
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-port/onboarding_profile_type_fragment.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/onboarding_profile_type_activity.xml b/app/src/main/res/layout/onboarding_profile_type_activity.xml
new file mode 100644
index 00000000000..a7f78951061
--- /dev/null
+++ b/app/src/main/res/layout/onboarding_profile_type_activity.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/onboarding_profile_type_fragment.xml b/app/src/main/res/layout/onboarding_profile_type_fragment.xml
new file mode 100644
index 00000000000..d14485ee6d8
--- /dev/null
+++ b/app/src/main/res/layout/onboarding_profile_type_fragment.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/color_palette.xml b/app/src/main/res/values-night/color_palette.xml
index 7c9347d590a..901d95b33df 100644
--- a/app/src/main/res/values-night/color_palette.xml
+++ b/app/src/main/res/values-night/color_palette.xml
@@ -231,6 +231,7 @@
@color/color_def_dark_green
@color/color_def_accessible_grey
+ @color/color_def_dark_jade
@color/color_def_greenish_black
@color/color_def_white
diff --git a/app/src/main/res/values/color_defs.xml b/app/src/main/res/values/color_defs.xml
index d6144b1d5ed..af6a0ba28cd 100644
--- a/app/src/main/res/values/color_defs.xml
+++ b/app/src/main/res/values/color_defs.xml
@@ -141,9 +141,12 @@
#F6F6F6
#BDCCCC
- #E8E8E8
+ #E8E8E8
#E2F5F4
#25000000
#EDF6F5
#172B28
+ #8EBBB6
+ #64817E
+ #F8BF74
diff --git a/app/src/main/res/values/color_palette.xml b/app/src/main/res/values/color_palette.xml
index 649cc14c2d9..abeed016fca 100644
--- a/app/src/main/res/values/color_palette.xml
+++ b/app/src/main/res/values/color_palette.xml
@@ -261,7 +261,7 @@
@color/color_def_pale_green
@color/color_def_light_blue
@color/color_def_accessible_grey
- @color/color_def_survey_disabled_button_grey
+ @color/color_def_disabled_button_grey
@color/color_def_chooser_grey
@color/color_def_persian_green
@color/color_def_grey
@@ -271,6 +271,9 @@
@color/color_def_oppia_green
@color/color_def_accessible_grey
+ @color/color_def_jade
+ @color/color_def_oppia_brown
+ @color/color_def_light_orange
@color/color_def_greenish_white
@color/color_def_green
diff --git a/app/src/main/res/values/component_colors.xml b/app/src/main/res/values/component_colors.xml
index fd03df0ed14..94dd01bd602 100644
--- a/app/src/main/res/values/component_colors.xml
+++ b/app/src/main/res/values/component_colors.xml
@@ -304,9 +304,13 @@
@color/color_palette_button_text_color
@color/color_palette_edit_text_unselected_color
@color/color_palette_button_shadow_color
+
@color/color_palette_white_text_color
@color/color_palette_onboarding_primary_color
@color/color_palette_onboarding_primary_text_color
+ @color/color_palette_onboarding_profile_type_background_color
+ @color/color_palette_learner_profile_type_background_color
+ @color/color_palette_supervisor_profile_type_background_color
@color/color_palette_classroom_card_color
@color/color_palette_classroom_shared_text_color
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 2d53f641df9..1f521f91bfe 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -803,7 +803,9 @@
8dp
4dp
2dp
+
4dp
+ 8dp
12sp
14sp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 409c502e500..307e20dcc92 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -646,6 +646,17 @@
You can change your language selection anytime in the app settings
Let\'s go!
+
+ Select Profile Type
+ Tell us more about you!
+ I\'m a student and I want to learn new things!
+ I\'m the parent, teacher, or guardian of a student.
+ STEP 2 OF 5
+
Cute otter wearing glasses.
+ Cute otter with books.
+ Mama and baby otter.
+ Back
+ Continue
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 4ca3883d1fe..c9bf5fae5eb 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -688,4 +688,47 @@
- @dimen/onboarding_shared_text_size_small
- sans-serif
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityTest.kt
new file mode 100644
index 00000000000..82c3d74ae11
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityTest.kt
@@ -0,0 +1,215 @@
+package org.oppia.android.app.onboarding
+
+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.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 [OnboardingProfileTypeActivity]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = OnboardingProfileTypeActivityTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class OnboardingProfileTypeActivityTest {
+ @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 =
+ OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(context)
+ .extractCurrentAppScreenName()
+
+ assertThat(screenName).isEqualTo(ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY)
+ }
+
+ @Test
+ fun testProfileTypeActivity_hasCorrectActivityLabel() {
+ launchOnboardingProfileTypeActivity().use { scenario ->
+ lateinit var title: CharSequence
+ scenario?.onActivity { activity -> title = activity.title }
+
+ // Verify that the activity label is correct as a proxy to verify TalkBack will announce the
+ // correct string when it's read out.
+ assertThat(title)
+ .isEqualTo(context.getString(R.string.onboarding_profile_type_activity_title))
+ }
+ }
+
+ private fun launchOnboardingProfileTypeActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(context)
+ )
+ 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(onboardingProfileTypeActivityTest: OnboardingProfileTypeActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerOnboardingProfileTypeActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(onboardingProfileTypeActivityTest: OnboardingProfileTypeActivityTest) {
+ component.inject(onboardingProfileTypeActivityTest)
+ }
+
+ 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/onboarding/OnboardingProfileTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
new file mode 100644
index 00000000000..ad8f036e214
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
@@ -0,0 +1,398 @@
+package org.oppia.android.app.onboarding
+
+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.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+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.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.hamcrest.CoreMatchers.allOf
+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.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.profile.ProfileChooserActivity
+import org.oppia.android.app.shim.ViewBindingShimModule
+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.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.platformparameter.TestPlatformParameterModule
+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.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.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 [OnboardingProfileTypeFragment]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = OnboardingProfileTypeFragmentTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class OnboardingProfileTypeFragmentTest {
+ @get:Rule
+ val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @Inject
+ lateinit var context: Context
+
+ @Inject
+ lateinit var machineLocale: OppiaLocale.MachineLocale
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ Intents.release()
+ }
+
+ @Test
+ fun testFragment_portraitMode_headerTextIsDisplayed() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(withId(R.id.profile_type_title))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ withText(
+ R.string.onboarding_profile_type_activity_header
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_headerTextIsDisplayed() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.profile_type_title))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ withText(
+ R.string.onboarding_profile_type_activity_header
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_navigationCardsAreDisplayed() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(withId(R.id.profile_type_learner_navigation_card))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ hasDescendant(
+ withText(R.string.onboarding_profile_type_activity_student_text)
+ )
+ )
+ )
+ )
+
+ onView(withId(R.id.profile_type_supervisor_navigation_card))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ hasDescendant(
+ withText(R.string.onboarding_profile_type_activity_parent_text)
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_navigationCardsAreDisplayed() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.profile_type_learner_navigation_card))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ hasDescendant(
+ withText(R.string.onboarding_profile_type_activity_student_text)
+ )
+ )
+ )
+ )
+
+ onView(withId(R.id.profile_type_supervisor_navigation_card))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ hasDescendant(
+ withText(R.string.onboarding_profile_type_activity_parent_text)
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_portrait_stepCountTextIsDisplayed() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(withId(R.id.onboarding_steps_count))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ withText(
+ R.string.onboarding_step_count_two
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_studentNavigationCardClicked_launchesCreateProfileScreen() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(withId(R.id.profile_type_learner_navigation_card)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ // Does nothing for now, but should fail once navigation is implemented in a future PR.
+ onView(withId(R.id.profile_type_learner_navigation_card))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.profile_type_supervisor_navigation_card))
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_orientationChange_studentNavigationCardClicked_launchesCreateProfileScreen() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.profile_type_learner_navigation_card)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ // Does nothing for now, but should fail once navigation is implemented in a future PR.
+ onView(withId(R.id.profile_type_learner_navigation_card))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.profile_type_supervisor_navigation_card))
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_supervisorNavigationCardClicked_launchesProfileChooserScreen() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(withId(R.id.profile_type_supervisor_navigation_card)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasComponent(ProfileChooserActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testFragment_orientationChange_supervisorCardClicked_launchesProfileChooserScreen() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.profile_type_supervisor_navigation_card)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasComponent(ProfileChooserActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testFragment_backButtonPressed_currentScreenIsDestroyed() {
+ launchOnboardingProfileTypeActivity().use { scenario ->
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_backButtonPressed_currentScreenIsDestroyed() {
+ launchOnboardingProfileTypeActivity().use { scenario ->
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ private fun launchOnboardingProfileTypeActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(context)
+ )
+ 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 = [
+ 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, 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,
+ 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
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(onboardingProfileTypeFragmentTest: OnboardingProfileTypeFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerOnboardingProfileTypeFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(onboardingProfileTypeFragmentTest: OnboardingProfileTypeFragmentTest) {
+ component.inject(onboardingProfileTypeFragmentTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
+}
diff --git a/model/src/main/proto/screens.proto b/model/src/main/proto/screens.proto
index 84518cbea6d..9e323e5aa72 100644
--- a/model/src/main/proto/screens.proto
+++ b/model/src/main/proto/screens.proto
@@ -161,6 +161,9 @@ enum ScreenName {
// Screen name value for the scenario when the classroom list activity is visible to the user.
CLASSROOM_LIST_ACTIVITY = 50;
+
+ // Screen name value for the scenario when the profile type activity is visible to the user.
+ ONBOARDING_PROFILE_TYPE_ACTIVITY = 51;
}
// Defines the current visible UI screen of the application.
diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto
index 6a32f0c51ab..9a9918f2f64 100644
--- a/scripts/assets/file_content_validation_checks.textproto
+++ b/scripts/assets/file_content_validation_checks.textproto
@@ -358,7 +358,9 @@ file_content_checks {
failure_message: "Only colors from component_colors.xml may be used in drawables except vector assets."
exempted_file_patterns: "app/src/main/res/drawable.*?/(ic_|lesson_thumbnail_graphic_).+?\\.xml"
exempted_file_patterns: "app/src/main/res/drawable/full_oppia_logo.xml"
+ exempted_file_patterns: "app/src/main/res/drawable/learner_otter.xml"
exempted_file_patterns: "app/src/main/res/drawable/otter.xml"
+ exempted_file_patterns: "app/src/main/res/drawable/parent_teacher_otter.xml"
exempted_file_patterns: "app/src/main/res/drawable/profile_image_shadow.xml"
exempted_file_patterns: "app/src/main/res/drawable/rounded_white_background_with_shadow.xml"
exempted_file_patterns: "app/src/main/res/drawable/selected_region_background.xml"
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 6721dcb339e..4f5600d8576 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -1066,6 +1066,14 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/ViewPagerSlide.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeActivityPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt"
test_file_not_required: true
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 ab091a7bba7..bc52286b4e7 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
@@ -855,6 +855,7 @@ class EventBundleCreator @Inject constructor(
ScreenName.FOREGROUND_SCREEN -> "foreground_screen"
ScreenName.SURVEY_ACTIVITY -> "survey_activity"
ScreenName.CLASSROOM_LIST_ACTIVITY -> "classroom_list_activity"
+ ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY -> "onboarding_profile_type_activity"
}
private fun AppLanguageSelection.toAnalyticsText(): String {
From df472536554d53f21181f3e564ea57c0074d7d79 Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Mon, 1 Jul 2024 01:30:19 +0300
Subject: [PATCH 2/6] Fix Part of #4938: Introduce Create profile screen
(#5380)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Explanation
Fix Part of #4938: Add a new Activity and associated Fragments and
Presenters to allow a new learner to create a profile.
This does not include domain changes.
- Learner should be able to click “Continue” if they have entered their
nickname, even without adding a profile picture.
- Learner should not be able to click “Continue” if they have not
entered their nickname and an error message should be displayed.
- The learner can select a profile picture.
Placeholder tests have been added to ensure navigation tests are not
forgotten. These will fail once navigation has been implemented.
## 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
||||
| --- | --- | --- |
|| Portrait | Landscape |
|Mobile Light
Mode|![Screenshot_1719774012](https://github.com/oppia/oppia-android/assets/59600948/189f7e62-8761-4d38-b859-e73df23d1221)|![Screenshot_1719774028](https://github.com/oppia/oppia-android/assets/59600948/9e8b01c5-4b60-40aa-94ae-da4093241759)|
|Tablet Dark
Mode|![Screenshot_1719774236](https://github.com/oppia/oppia-android/assets/59600948/e5e161f9-77e7-4b1d-a2bc-b7b0646b71c2)|![Screenshot_1719774244](https://github.com/oppia/oppia-android/assets/59600948/97820f77-a628-48b0-a325-f929118594bc)|
## All Tests Passing on Espresso
![Screenshot 2024-05-24 at 01 42
41](https://github.com/oppia/oppia-android/assets/59600948/9e301e33-d11f-48a5-9f55-fca3e155b00e)
---
app/BUILD.bazel | 1 +
app/src/main/AndroidManifest.xml | 5 +-
.../app/activity/ActivityComponentImpl.kt | 2 +
.../app/fragment/FragmentComponentImpl.kt | 2 +
.../app/onboarding/CreateProfileActivity.kt | 32 +
.../CreateProfileActivityPresenter.kt | 39 ++
.../app/onboarding/CreateProfileFragment.kt | 38 ++
.../CreateProfileFragmentPresenter.kt | 120 ++++
.../app/onboarding/CreateProfileViewModel.kt | 14 +
.../OnboardingProfileTypeFragmentPresenter.kt | 6 +
.../drawable/create_profile_picture_icon.xml | 33 ++
...dit_text_white_background_error_border.xml | 9 +
...edit_text_white_background_with_border.xml | 9 +
.../main/res/drawable/ic_outline_edit_24.xml | 5 +
app/src/main/res/drawable/ic_profile_icon.xml | 9 +
.../layout-land/create_profile_fragment.xml | 155 +++++
.../create_profile_fragment.xml | 153 +++++
.../create_profile_fragment.xml | 159 +++++
.../res/layout/create_profile_activity.xml | 10 +
.../res/layout/create_profile_fragment.xml | 163 ++++++
.../main/res/values-night/color_palette.xml | 9 +-
app/src/main/res/values/color_defs.xml | 2 +-
app/src/main/res/values/color_palette.xml | 7 +-
app/src/main/res/values/component_colors.xml | 7 +-
app/src/main/res/values/strings.xml | 10 +
app/src/main/res/values/styles.xml | 46 ++
.../onboarding/CreateProfileActivityTest.kt | 214 +++++++
.../onboarding/CreateProfileFragmentTest.kt | 553 ++++++++++++++++++
.../OnboardingProfileTypeFragmentTest.kt | 7 +-
model/src/main/proto/screens.proto | 3 +
scripts/assets/test_file_exemptions.textproto | 12 +
.../util/logging/EventBundleCreator.kt | 1 +
32 files changed, 1823 insertions(+), 12 deletions(-)
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivity.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragment.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
create mode 100644 app/src/main/res/drawable/create_profile_picture_icon.xml
create mode 100644 app/src/main/res/drawable/edit_text_white_background_error_border.xml
create mode 100644 app/src/main/res/drawable/edit_text_white_background_with_border.xml
create mode 100644 app/src/main/res/drawable/ic_outline_edit_24.xml
create mode 100644 app/src/main/res/drawable/ic_profile_icon.xml
create mode 100644 app/src/main/res/layout-land/create_profile_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
create mode 100644 app/src/main/res/layout/create_profile_activity.xml
create mode 100644 app/src/main/res/layout/create_profile_fragment.xml
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
diff --git a/app/BUILD.bazel b/app/BUILD.bazel
index 7ddcebe5300..26ee5509faf 100644
--- a/app/BUILD.bazel
+++ b/app/BUILD.bazel
@@ -211,6 +211,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/home/recentlyplayed/PromotedStoryViewModel.kt",
"src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedViewModel.kt",
"src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt",
+ "src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt",
"src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt",
"src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt",
"src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt",
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b25b8ccfc86..0c545bf411e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -340,7 +340,10 @@
android:name=".app.onboarding.OnboardingProfileTypeActivity"
android:label="@string/onboarding_profile_type_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
-
+
+ if (result.resultCode == Activity.RESULT_OK) {
+ createProfileFragmentPresenter.handleOnActivityResult(result.data)
+ }
+ }
+ return createProfileFragmentPresenter.handleCreateView(inflater, container)
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
new file mode 100644
index 00000000000..d3a57c61988
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
@@ -0,0 +1,120 @@
+package org.oppia.android.app.onboarding
+
+import android.content.Intent
+import android.graphics.PorterDuff
+import android.provider.MediaStore
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.Fragment
+import org.oppia.android.R
+import org.oppia.android.app.fragment.FragmentScope
+import org.oppia.android.databinding.CreateProfileFragmentBinding
+import org.oppia.android.util.parser.image.ImageLoader
+import org.oppia.android.util.parser.image.ImageViewTarget
+import javax.inject.Inject
+
+/** Presenter for [CreateProfileFragment]. */
+@FragmentScope
+class CreateProfileFragmentPresenter @Inject constructor(
+ private val fragment: Fragment,
+ private val activity: AppCompatActivity,
+ private val createProfileViewModel: CreateProfileViewModel,
+ private val imageLoader: ImageLoader
+) {
+ private lateinit var binding: CreateProfileFragmentBinding
+ private lateinit var uploadImageView: ImageView
+ private lateinit var selectedImage: String
+
+ /** Launcher for picking an image from device gallery. */
+ lateinit var activityResultLauncher: ActivityResultLauncher
+
+ /** Initialize layout bindings. */
+ fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View {
+ binding = CreateProfileFragmentBinding.inflate(
+ inflater,
+ container,
+ /* attachToRoot= */ false
+ )
+ binding.let {
+ it.lifecycleOwner = fragment
+ it.viewModel = createProfileViewModel
+ }
+
+ uploadImageView = binding.createProfileUserImageView
+
+ uploadImageView.apply {
+ setColorFilter(
+ ResourcesCompat.getColor(
+ activity.resources,
+ R.color.component_color_avatar_background_25_color,
+ null
+ ),
+ PorterDuff.Mode.DST_OVER
+ )
+
+ imageLoader.loadDrawable(
+ R.drawable.ic_profile_icon,
+ ImageViewTarget(this)
+ )
+ }
+
+ binding.onboardingNavigationContinue.setOnClickListener {
+ val nickname = binding.createProfileNicknameEdittext.text.toString().trim()
+
+ createProfileViewModel.hasErrorMessage.set(nickname.isBlank())
+ }
+
+ binding.createProfileNicknameEdittext.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun afterTextChanged(s: Editable?) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ createProfileViewModel.hasErrorMessage.set(false)
+ }
+ })
+
+ addViewOnClickListeners(binding)
+
+ return binding.root
+ }
+
+ /** Receive the result of image upload and load it into the image view. */
+ fun handleOnActivityResult(intent: Intent?) {
+ intent?.let {
+ binding.createProfilePicturePrompt.visibility = View.GONE
+ selectedImage =
+ checkNotNull(intent.data.toString()) { "Could not find the selected image." }
+ imageLoader.loadBitmap(
+ selectedImage,
+ ImageViewTarget(uploadImageView)
+ )
+ }
+ }
+
+ private fun addViewOnClickListeners(binding: CreateProfileFragmentBinding) {
+ val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+
+ binding.onboardingNavigationBack.setOnClickListener { activity.finish() }
+ binding.createProfileEditPictureIcon.setOnClickListener {
+ activityResultLauncher.launch(
+ galleryIntent
+ )
+ }
+ binding.createProfilePicturePrompt.setOnClickListener {
+ activityResultLauncher.launch(
+ galleryIntent
+ )
+ }
+ binding.createProfileUserImageView.setOnClickListener {
+ activityResultLauncher.launch(
+ galleryIntent
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
new file mode 100644
index 00000000000..e6ef763f23c
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
@@ -0,0 +1,14 @@
+package org.oppia.android.app.onboarding
+
+import androidx.databinding.ObservableField
+import org.oppia.android.app.fragment.FragmentScope
+import org.oppia.android.app.viewmodel.ObservableViewModel
+import javax.inject.Inject
+
+/** The ViewModel for [CreateProfileFragment]. */
+@FragmentScope
+class CreateProfileViewModel @Inject constructor() : ObservableViewModel() {
+
+ /** ObservableField that tracks whether creating a nickname has triggered an error condition. */
+ val hasErrorMessage = ObservableField(false)
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
index 893960b55c7..72ae543dd0c 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
@@ -27,6 +27,11 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
binding.apply {
lifecycleOwner = fragment
+ profileTypeLearnerNavigationCard.setOnClickListener {
+ val intent = CreateProfileActivity.createProfileActivityIntent(activity)
+ fragment.startActivity(intent)
+ }
+
profileTypeSupervisorNavigationCard.setOnClickListener {
val intent = ProfileChooserActivity.createProfileChooserActivity(activity)
fragment.startActivity(intent)
@@ -36,6 +41,7 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
activity.finish()
}
}
+
return binding.root
}
}
diff --git a/app/src/main/res/drawable/create_profile_picture_icon.xml b/app/src/main/res/drawable/create_profile_picture_icon.xml
new file mode 100644
index 00000000000..0aa1cd192f0
--- /dev/null
+++ b/app/src/main/res/drawable/create_profile_picture_icon.xml
@@ -0,0 +1,33 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/edit_text_white_background_error_border.xml b/app/src/main/res/drawable/edit_text_white_background_error_border.xml
new file mode 100644
index 00000000000..2851e1fc4cb
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_white_background_error_border.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/edit_text_white_background_with_border.xml b/app/src/main/res/drawable/edit_text_white_background_with_border.xml
new file mode 100644
index 00000000000..90e111c7c1a
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_white_background_with_border.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_edit_24.xml b/app/src/main/res/drawable/ic_outline_edit_24.xml
new file mode 100644
index 00000000000..407f3e2f737
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_edit_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_icon.xml b/app/src/main/res/drawable/ic_profile_icon.xml
new file mode 100644
index 00000000000..7b7c22f999b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout-land/create_profile_fragment.xml b/app/src/main/res/layout-land/create_profile_fragment.xml
new file mode 100644
index 00000000000..93c01c2f32d
--- /dev/null
+++ b/app/src/main/res/layout-land/create_profile_fragment.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml b/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
new file mode 100644
index 00000000000..5eba49ebb23
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml b/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
new file mode 100644
index 00000000000..0edd5932959
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/create_profile_activity.xml b/app/src/main/res/layout/create_profile_activity.xml
new file mode 100644
index 00000000000..c61355aa7d5
--- /dev/null
+++ b/app/src/main/res/layout/create_profile_activity.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/create_profile_fragment.xml b/app/src/main/res/layout/create_profile_fragment.xml
new file mode 100644
index 00000000000..ab53fbfc69a
--- /dev/null
+++ b/app/src/main/res/layout/create_profile_fragment.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/color_palette.xml b/app/src/main/res/values-night/color_palette.xml
index 901d95b33df..19e760c1943 100644
--- a/app/src/main/res/values-night/color_palette.xml
+++ b/app/src/main/res/values-night/color_palette.xml
@@ -48,7 +48,7 @@
@color/color_def_oppia_grayish_black
@color/color_def_dark_blue
@color/color_def_oppia_grayish_black
- @color/color_def_oppia_grey_border
+ @color/color_def_oppia_grey
@color/color_def_oppia_turquoise
@color/color_def_white
@color/color_def_white
@@ -231,7 +231,12 @@
@color/color_def_dark_green
@color/color_def_accessible_grey
- @color/color_def_dark_jade
+ @color/color_def_jade
+ @color/color_def_oppia_green
+ @color/color_def_black
+
+ @color/color_def_oppia_reddish_brown
+ @color/color_def_accessible_light_grey_2
@color/color_def_greenish_black
@color/color_def_white
diff --git a/app/src/main/res/values/color_defs.xml b/app/src/main/res/values/color_defs.xml
index af6a0ba28cd..7e09cb35dce 100644
--- a/app/src/main/res/values/color_defs.xml
+++ b/app/src/main/res/values/color_defs.xml
@@ -63,7 +63,7 @@
#FF938F
#32363B
#395FD0
- #DDDDDD
+ #DDDDDD
#EEEEEE
#707070
#FFFFF0
diff --git a/app/src/main/res/values/color_palette.xml b/app/src/main/res/values/color_palette.xml
index abeed016fca..f31cfb8734e 100644
--- a/app/src/main/res/values/color_palette.xml
+++ b/app/src/main/res/values/color_palette.xml
@@ -51,7 +51,7 @@
@color/color_def_oppia_solid_blue
@color/color_def_oppia_stroke_blue
@color/color_def_white
- @color/color_def_oppia_grey_border
+ @color/color_def_oppia_grey
@color/color_def_oppia_dark_blue
@color/color_def_oppia_dark_blue
@color/color_def_oppia_dark_blue
@@ -244,6 +244,7 @@
@color/color_def_avatar_background_22
@color/color_def_avatar_background_23
@color/color_def_avatar_background_24
+ @color/color_def_oppia_grey
@color/color_def_white_f5
@color/color_def_white_f6
@@ -274,6 +275,10 @@
@color/color_def_jade
@color/color_def_oppia_brown
@color/color_def_light_orange
+ @color/color_def_persian_green
+ @color/color_def_black
+ @color/color_def_accessible_light_grey_2
+ @color/color_def_error_text
@color/color_def_greenish_white
@color/color_def_green
diff --git a/app/src/main/res/values/component_colors.xml b/app/src/main/res/values/component_colors.xml
index 94dd01bd602..7ba51ce5092 100644
--- a/app/src/main/res/values/component_colors.xml
+++ b/app/src/main/res/values/component_colors.xml
@@ -148,6 +148,7 @@
@color/color_palette_avatar_background_22_color
@color/color_palette_avatar_background_23_color
@color/color_palette_avatar_background_24_color
+ @color/color_palette_avatar_background_25_color
@color/color_palette_forgot_pin_color
@color/color_palette_show_hide_color
@@ -307,10 +308,14 @@
@color/color_palette_white_text_color
@color/color_palette_onboarding_primary_color
+ @color/color_palette_onboarding_black_color
@color/color_palette_onboarding_primary_text_color
@color/color_palette_onboarding_profile_type_background_color
@color/color_palette_learner_profile_type_background_color
- @color/color_palette_supervisor_profile_type_background_color
+ @color/color_palette_supervisor_profile_type_background_color
+ @color/color_palette_onboarding_edit_icon_color
+ @color/color_palette_edittext_stroke_color
+ @color/color_palette_text_error_color
@color/color_palette_classroom_card_color
@color/color_palette_classroom_shared_text_color
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 307e20dcc92..7dac7187713 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -653,6 +653,16 @@
I\'m the parent, teacher, or guardian of a student.
STEP 2 OF 5
+
+ Create Profile
+ What should we call you?
+ Nickname
+ Tap here to add a picture
+ Click in the box above to type your nickname.
+ Edit profile picture
+ Current profile picture
+ STEP 3 OF 5
+
Cute otter wearing glasses.
Cute otter with books.
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c9bf5fae5eb..0123d3d0b7b 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -731,4 +731,50 @@
- @color/component_color_onboarding_shared_green_color
- @dimen/onboarding_shared_text_size_medium
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt
new file mode 100644
index 00000000000..3763cf7d57c
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt
@@ -0,0 +1,214 @@
+package org.oppia.android.app.onboarding
+
+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.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 [CreateProfileActivity]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = CreateProfileActivityTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class CreateProfileActivityTest {
+ @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 =
+ CreateProfileActivity.createProfileActivityIntent(context)
+ .extractCurrentAppScreenName()
+
+ assertThat(screenName).isEqualTo(ScreenName.CREATE_PROFILE_ACTIVITY)
+ }
+
+ @Test
+ fun testNewLearnerProfileActivity_hasCorrectActivityLabel() {
+ launchNewLearnerProfileActivity().use { scenario ->
+ lateinit var title: CharSequence
+ scenario?.onActivity { activity -> title = activity.title }
+
+ // Verify that the activity label is correct as a proxy to verify TalkBack will announce the
+ // correct string when it's read out.
+ assertThat(title).isEqualTo(context.getString(R.string.create_profile_activity_title))
+ }
+ }
+
+ private fun launchNewLearnerProfileActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ CreateProfileActivity.createProfileActivityIntent(context)
+ )
+ 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(newLearnerProfileActivityTest: CreateProfileActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerCreateProfileActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(newLearnerProfileActivityTest: CreateProfileActivityTest) {
+ component.inject(newLearnerProfileActivityTest)
+ }
+
+ 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/onboarding/CreateProfileFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
new file mode 100644
index 00000000000..40f782fbd55
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
@@ -0,0 +1,553 @@
+package org.oppia.android.app.onboarding
+
+import android.app.Activity
+import android.app.Application
+import android.app.Instrumentation
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.net.Uri
+import androidx.appcompat.app.AppCompatActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
+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.Intents.intending
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.not
+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.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+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.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.espresso.EditTextInputAction
+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.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.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.oppia.android.util.parser.image.TestGlideImageLoader
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [CreateProfileFragment]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = CreateProfileFragmentTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class CreateProfileFragmentTest {
+ @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+ @get:Rule val oppiaTestRule = OppiaTestRule()
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject lateinit var context: Context
+ @Inject lateinit var editTextInputAction: EditTextInputAction
+ @Inject lateinit var testGlideImageLoader: TestGlideImageLoader
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ Intents.release()
+ }
+
+ @Test
+ fun testFragment_nicknameLabelIsDisplayed() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_label))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ withText(
+ context.getString(
+ R.string.create_profile_activity_nickname_label
+ )
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_stepCountText_isDisplayed() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_steps_count))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ withText(
+ context.getString(
+ R.string.onboarding_step_count_three
+ )
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_filledNickname_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(withEffectiveVisibility(Visibility.GONE)))
+
+ // No screen change as the navigation to the next screen is not implemented yet.
+ // This should fail in the future once the screen has been implemented.
+ onView(withId(R.id.create_profile_nickname_label))
+ .check(
+ matches(
+ withText(
+ context.getString(
+ R.string.create_profile_activity_nickname_label
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_filledNickname_doesNotShowErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(withEffectiveVisibility(Visibility.GONE)))
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_emptyNickname_showNicknameErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_filledNickname_afterError_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // No screen change as the navigation to the next screen is not implemented yet.
+ // This should fail in the future once the screen has been implemented.
+ onView(withId(R.id.create_profile_nickname_label))
+ .check(
+ matches(
+ withText(
+ context.getString(
+ R.string.create_profile_activity_nickname_label
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_onTextChanged_afterError_hidesErrorMessage() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(not(isDisplayed())))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_filledNickname_continueButtonClicked_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // No screen change as the navigation to the next screen is not implemented yet.
+ // This should fail in the future once the screen has been implemented.
+ onView(withId(R.id.create_profile_nickname_label))
+ .check(
+ matches(
+ withText(
+ context.getString(
+ R.string.create_profile_activity_nickname_label
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_filledNickname_continueButtonClicked_doesNotShowErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(withEffectiveVisibility(Visibility.GONE)))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_continueButtonClicked_emptyNickname_showNicknameErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_landscape_continueButtonClicked_afterErrorShown_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // No screen change as the navigation to the next screen is not implemented yet.
+ // This should fail in the future once the screen has been implemented.
+ onView(withId(R.id.create_profile_nickname_label))
+ .check(
+ matches(
+ withText(
+ context.getString(
+ R.string.create_profile_activity_nickname_label
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_backButtonPressed_currentScreenIsDestroyed() {
+ launchNewLearnerProfileActivity().use { scenario ->
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_backButtonPressed_currentScreenIsDestroyed() {
+ launchNewLearnerProfileActivity().use { scenario ->
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_tapToAddPictureClicked_hasGalleryIntent() {
+ launchNewLearnerProfileActivity().use {
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasAction(Intent.ACTION_PICK))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_tapToAddPictureClicked_hasGalleryIntent() {
+ launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasAction(Intent.ACTION_PICK))
+ }
+ }
+
+ @Test
+ fun testFragment_tapToAddPictureClicked_loadsTheImageFromGallery() {
+ val expectedIntent: Matcher = hasAction(Intent.ACTION_PICK)
+
+ val activityResult = createGalleryPickActivityResultStub()
+ intending(expectedIntent).respondWith(activityResult)
+
+ launchNewLearnerProfileActivity().use {
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ val loadedImageUri = activityResult.resultData.data.toString()
+ assertThat(loadedImageUri).contains("launcher_icon")
+ }
+ }
+
+ @Test
+ fun testFragment_uploadProfilePicture_displaysImageInTarget() {
+ val expectedIntent: Matcher = hasAction(Intent.ACTION_PICK)
+
+ val activityResult = createGalleryPickActivityResultStub()
+ intending(expectedIntent).respondWith(activityResult)
+
+ launchNewLearnerProfileActivity().use {
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ val expectedImage = activityResult.resultData.data.toString()
+ val loadedImages = testGlideImageLoader.getLoadedBitmaps()
+ assertThat(loadedImages.first()).isEqualTo(expectedImage)
+ }
+ }
+
+ private fun createGalleryPickActivityResultStub(): Instrumentation.ActivityResult {
+ val resources: Resources = context.resources
+ val imageUri = Uri.parse(
+ ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
+ resources.getResourcePackageName(R.mipmap.launcher_icon) + '/' +
+ resources.getResourceTypeName(R.mipmap.launcher_icon) + '/' +
+ resources.getResourceEntryName(R.mipmap.launcher_icon)
+ )
+ val resultIntent = Intent()
+ resultIntent.data = imageUri
+ return Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent)
+ }
+
+ private fun launchNewLearnerProfileActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ CreateProfileActivity.createProfileActivityIntent(context)
+ )
+ 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 = [
+ 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(newLearnerProfileFragmentTest: CreateProfileFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerCreateProfileFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(newLearnerProfileFragmentTest: CreateProfileFragmentTest) {
+ component.inject(newLearnerProfileFragmentTest)
+ }
+
+ 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/onboarding/OnboardingProfileTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
index ad8f036e214..2649e11c610 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
@@ -273,12 +273,7 @@ class OnboardingProfileTypeFragmentTest {
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.profile_type_learner_navigation_card)).perform(click())
testCoroutineDispatchers.runCurrent()
- // Does nothing for now, but should fail once navigation is implemented in a future PR.
- onView(withId(R.id.profile_type_learner_navigation_card))
- .check(matches(isDisplayed()))
-
- onView(withId(R.id.profile_type_supervisor_navigation_card))
- .check(matches(isDisplayed()))
+ intended(hasComponent(CreateProfileActivity::class.java.name))
}
}
diff --git a/model/src/main/proto/screens.proto b/model/src/main/proto/screens.proto
index 9e323e5aa72..48631a4a3e4 100644
--- a/model/src/main/proto/screens.proto
+++ b/model/src/main/proto/screens.proto
@@ -164,6 +164,9 @@ enum ScreenName {
// Screen name value for the scenario when the profile type activity is visible to the user.
ONBOARDING_PROFILE_TYPE_ACTIVITY = 51;
+
+ // Screen name value for the scenario when the create new learner profile activity is visible to the user.
+ CREATE_PROFILE_ACTIVITY = 52;
}
// Defines the current visible UI screen of the application.
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 4f5600d8576..107658a4fcc 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -1074,6 +1074,18 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt"
test_file_not_required: true
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 bc52286b4e7..24f5db6dabf 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
@@ -856,6 +856,7 @@ class EventBundleCreator @Inject constructor(
ScreenName.SURVEY_ACTIVITY -> "survey_activity"
ScreenName.CLASSROOM_LIST_ACTIVITY -> "classroom_list_activity"
ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY -> "onboarding_profile_type_activity"
+ ScreenName.CREATE_PROFILE_ACTIVITY -> "create_profile_activity"
}
private fun AppLanguageSelection.toAnalyticsText(): String {
From d8f76351203421ed099c0663ebe0841827218aa0 Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Mon, 1 Jul 2024 02:39:12 +0300
Subject: [PATCH 3/6] Fix Part of #4938: Introduce Onboarding Intro screen
(#5385)
## Explanation
Fix Part of https://github.com/oppia/oppia-android/issues/4938: New
screen to explain to the learner the purpose of the app.
The new learner's name is added as a greeting, passed via the intent.
Tests and placeholder tests have been added to verify that all views are
displayed and navigation works as expected.
## 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
||Portrait|Landscape|
|---|---|---|
|Mobile Light
Mode|![Screenshot_1718117878](https://github.com/oppia/oppia-android/assets/59600948/d01960a0-8eb4-4067-99ac-ccaa2f3b54af)|![Screenshot_1718117885](https://github.com/oppia/oppia-android/assets/59600948/29bb0cae-461e-40f1-930b-d3a2c58e1082)|
|Mobile Dark
Mode|![Screenshot_1718117908](https://github.com/oppia/oppia-android/assets/59600948/5fa43dce-fc9f-4924-ac10-1f1d55865580)|![Screenshot_1718117912](https://github.com/oppia/oppia-android/assets/59600948/e1fc1a9d-e993-4f8f-aa49-970b65dd444b)|
|Tablet Light
Mode|![Screenshot_1718118054](https://github.com/oppia/oppia-android/assets/59600948/4ebd9113-237b-4598-a1b9-982345bff8fd)|![Screenshot_1718118045](https://github.com/oppia/oppia-android/assets/59600948/bf318732-baa2-4f64-aa83-b1f0e82a8ba8)|![Screenshot_1718118119](https://github.com/oppia/oppia-android/assets/59600948/a0628419-867c-405a-b548-f487c1298e76)|
|Tablet Dark
Mode|![Screenshot_1718118119](https://github.com/oppia/oppia-android/assets/59600948/fdd40f9b-7cdf-4878-8833-f4d02500a3c2)|![Screenshot_1718118077](https://github.com/oppia/oppia-android/assets/59600948/3b4112a4-6574-432a-9d85-3a39767385a7)|
---
app/src/main/AndroidManifest.xml | 4 +
.../app/activity/ActivityComponentImpl.kt | 2 +
.../app/fragment/FragmentComponentImpl.kt | 2 +
.../CreateProfileFragmentPresenter.kt | 5 +
.../android/app/onboarding/IntroActivity.kt | 59 ++++
.../app/onboarding/IntroActivityPresenter.kt | 49 +++
.../android/app/onboarding/IntroFragment.kt | 38 +++
.../app/onboarding/IntroFragmentPresenter.kt | 55 ++++
app/src/main/res/drawable/ic_green_check.xml | 9 +
.../layout-land/learner_intro_fragment.xml | 96 ++++++
.../learner_intro_fragment.xml | 100 ++++++
.../learner_intro_fragment.xml | 108 +++++++
app/src/main/res/layout/intro_activity.xml | 10 +
.../res/layout/learner_intro_fragment.xml | 114 +++++++
.../main/res/values-night/color_palette.xml | 4 +
app/src/main/res/values/color_defs.xml | 1 +
app/src/main/res/values/color_palette.xml | 5 +
app/src/main/res/values/component_colors.xml | 4 +
app/src/main/res/values/strings.xml | 8 +
app/src/main/res/values/styles.xml | 11 +
.../onboarding/CreateProfileFragmentTest.kt | 91 +++---
.../app/onboarding/IntroActivityTest.kt | 224 +++++++++++++
.../app/onboarding/IntroFragmentTest.kt | 304 ++++++++++++++++++
model/src/main/proto/arguments.proto | 6 +
model/src/main/proto/screens.proto | 3 +
scripts/assets/test_file_exemptions.textproto | 7 +
.../util/logging/EventBundleCreator.kt | 1 +
27 files changed, 1273 insertions(+), 47 deletions(-)
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/IntroActivity.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/IntroFragment.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt
create mode 100644 app/src/main/res/drawable/ic_green_check.xml
create mode 100644 app/src/main/res/layout-land/learner_intro_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-land/learner_intro_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-port/learner_intro_fragment.xml
create mode 100644 app/src/main/res/layout/intro_activity.xml
create mode 100644 app/src/main/res/layout/learner_intro_fragment.xml
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0c545bf411e..f036a892fbf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -344,6 +344,10 @@
android:name=".app.onboarding.CreateProfileActivity"
android:label="@string/create_profile_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
+
+
+
diff --git a/app/src/main/res/layout-land/learner_intro_fragment.xml b/app/src/main/res/layout-land/learner_intro_fragment.xml
new file mode 100644
index 00000000000..1e1beb75386
--- /dev/null
+++ b/app/src/main/res/layout-land/learner_intro_fragment.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-land/learner_intro_fragment.xml b/app/src/main/res/layout-sw600dp-land/learner_intro_fragment.xml
new file mode 100644
index 00000000000..45ada6f06a3
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/learner_intro_fragment.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-port/learner_intro_fragment.xml b/app/src/main/res/layout-sw600dp-port/learner_intro_fragment.xml
new file mode 100644
index 00000000000..59f9d0c3097
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-port/learner_intro_fragment.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/intro_activity.xml b/app/src/main/res/layout/intro_activity.xml
new file mode 100644
index 00000000000..85514b1b723
--- /dev/null
+++ b/app/src/main/res/layout/intro_activity.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/learner_intro_fragment.xml b/app/src/main/res/layout/learner_intro_fragment.xml
new file mode 100644
index 00000000000..7f0b4be7583
--- /dev/null
+++ b/app/src/main/res/layout/learner_intro_fragment.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/color_palette.xml b/app/src/main/res/values-night/color_palette.xml
index 19e760c1943..bed7ab257be 100644
--- a/app/src/main/res/values-night/color_palette.xml
+++ b/app/src/main/res/values-night/color_palette.xml
@@ -234,6 +234,10 @@
@color/color_def_jade
@color/color_def_oppia_green
@color/color_def_black
+ @color/color_def_highlight_blue_darker
+ @color/color_def_oppia_turquoise
+ @color/color_def_sky_blue
+ @color/color_def_oppia_turquoise
@color/color_def_oppia_reddish_brown
@color/color_def_accessible_light_grey_2
diff --git a/app/src/main/res/values/color_defs.xml b/app/src/main/res/values/color_defs.xml
index 7e09cb35dce..4eb98853686 100644
--- a/app/src/main/res/values/color_defs.xml
+++ b/app/src/main/res/values/color_defs.xml
@@ -149,4 +149,5 @@
#8EBBB6
#64817E
#F8BF74
+ #B3D8F1
diff --git a/app/src/main/res/values/color_palette.xml b/app/src/main/res/values/color_palette.xml
index f31cfb8734e..cb6a86c2ff9 100644
--- a/app/src/main/res/values/color_palette.xml
+++ b/app/src/main/res/values/color_palette.xml
@@ -277,6 +277,11 @@
@color/color_def_light_orange
@color/color_def_persian_green
@color/color_def_black
+ @color/color_def_sky_blue
+ @color/color_def_oppia_green
+ @color/color_def_black
+ @color/color_def_oppia_green
+
@color/color_def_accessible_light_grey_2
@color/color_def_error_text
diff --git a/app/src/main/res/values/component_colors.xml b/app/src/main/res/values/component_colors.xml
index 7ba51ce5092..6ed6c6a5df8 100644
--- a/app/src/main/res/values/component_colors.xml
+++ b/app/src/main/res/values/component_colors.xml
@@ -314,6 +314,10 @@
@color/color_palette_learner_profile_type_background_color
@color/color_palette_supervisor_profile_type_background_color
@color/color_palette_onboarding_edit_icon_color
+ @color/color_palette_onboarding_learner_intro_background_color
+ @color/color_palette_onboarding_learner_intro_header_color
+ @color/color_palette_onboarding_learner_intro_list_color
+ @color/color_palette_onboarding_learner_intro_check_color
@color/color_palette_edittext_stroke_color
@color/color_palette_text_error_color
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7dac7187713..e935ea76583 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -663,6 +663,14 @@
Current profile picture
STEP 3 OF 5
+
+ Welcome
+ Welcome, %s!
+ Learn Math through fun, story-based lessons.
+ Try practice questions to test your knowledge.
+ Get feedback to improve using %s\'s corrections.
+ STEP 4 OF 5
+
Cute otter wearing glasses.
Cute otter with books.
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 0123d3d0b7b..2231353378b 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -777,4 +777,15 @@
- @dimen/onboarding_shared_text_size_medium
- @color/component_color_shared_error_color
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
index 40f782fbd55..5e9c8ada80e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
@@ -19,6 +19,7 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
@@ -27,10 +28,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import com.google.protobuf.MessageLite
import dagger.Component
+import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
+import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -48,6 +52,7 @@ 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.IntroActivityParams
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
@@ -96,6 +101,7 @@ 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.logging.EventLoggingConfigurationModule
@@ -193,21 +199,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- onView(withText(R.string.create_profile_activity_nickname_error))
- .check(matches(withEffectiveVisibility(Visibility.GONE)))
-
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -259,18 +257,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -307,18 +300,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -373,18 +361,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -490,6 +473,20 @@ class CreateProfileFragmentTest {
return scenario
}
+ 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
+ }
+ }
+ }
+
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
new file mode 100644
index 00000000000..11ded15d116
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
@@ -0,0 +1,224 @@
+package org.oppia.android.app.onboarding
+
+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.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 [IntroActivity]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = IntroActivityTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+
+class IntroActivityTest {
+ @get:Rule
+ val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
+
+ @Inject
+ lateinit var context: Context
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ private val testProfileNickname = "John"
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ }
+
+ @Test
+ fun testActivity_createIntent_verifyScreenNameInIntent() {
+ val screenName =
+ IntroActivity.createIntroActivity(
+ context,
+ testProfileNickname
+ )
+ .extractCurrentAppScreenName()
+
+ assertThat(screenName).isEqualTo(ScreenName.INTRO_ACTIVITY)
+ }
+
+ @Test
+ fun testLearnerIntroActivity_hasCorrectActivityLabel() {
+ launchOnboardingLearnerIntroActivity().use { scenario ->
+ lateinit var title: CharSequence
+ scenario?.onActivity { activity -> title = activity.title }
+
+ // Verify that the activity label is correct as a proxy to verify TalkBack will announce the
+ // correct string when it's read out.
+ assertThat(title)
+ .isEqualTo(context.getString(R.string.onboarding_learner_intro_activity_title))
+ }
+ }
+
+ private fun launchOnboardingLearnerIntroActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ IntroActivity.createIntroActivity(
+ context,
+ testProfileNickname
+ )
+ )
+ 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(introActivityTest: IntroActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerIntroActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(introActivityTest: IntroActivityTest) {
+ component.inject(introActivityTest)
+ }
+
+ 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/onboarding/IntroFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
new file mode 100644
index 00000000000..72fea853fbc
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
@@ -0,0 +1,304 @@
+package org.oppia.android.app.onboarding
+
+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.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+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.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+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.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.platformparameter.TestPlatformParameterModule
+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.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 [IntroFragmentTest]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = IntroFragmentTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class IntroFragmentTest {
+ @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+ @get:Rule val oppiaTestRule = OppiaTestRule()
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject lateinit var context: Context
+
+ private val testProfileNickname = "John"
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ Intents.release()
+ }
+
+ @Test
+ fun testFragment_explanationText_isDisplayed() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(withId(R.id.onboarding_learner_intro_title))
+ .check(matches(withText("Welcome, John!")))
+ onView(withText(R.string.onboarding_learner_intro_classroom_text))
+ .check(matches(isDisplayed()))
+ onView(withText(R.string.onboarding_learner_intro_practice_text))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ context.getString(
+ R.string.onboarding_learner_intro_feedback_text,
+ context.getString(R.string.app_name)
+ )
+ )
+ ).check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_stepCountText_isDisplayed() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(withId(R.id.onboarding_steps_count))
+ .check(matches(isDisplayed()))
+ onView(withId(R.id.onboarding_steps_count))
+ .check(matches(withText(R.string.onboarding_step_count_four)))
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_backButtonPressed_currentScreenIsDestroyed() {
+ launchOnboardingLearnerIntroActivity().use { scenario ->
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_backButtonPressed_currentScreenIsDestroyed() {
+ launchOnboardingLearnerIntroActivity().use { scenario ->
+ onView(ViewMatchers.isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_continueButtonClicked_launchesAudioLanguageScreen() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // Do nothing for now, but will fail once navigation is implemented
+ onView(withId(R.id.onboarding_learner_intro_title))
+ .check(matches(withText("Welcome, John!")))
+ onView(withText(R.string.onboarding_learner_intro_classroom_text))
+ .check(matches(isDisplayed()))
+ onView(withText(R.string.onboarding_learner_intro_practice_text))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ context.getString(
+ R.string.onboarding_learner_intro_feedback_text,
+ context.getString(R.string.app_name)
+ )
+ )
+ ).check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_continueButtonClicked_launchesAudioLanguageScreen() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(ViewMatchers.isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // Do nothing for now, but will fail once navigation is implemented
+ onView(withId(R.id.onboarding_learner_intro_title))
+ .check(matches(withText("Welcome, John!")))
+ onView(withText(R.string.onboarding_learner_intro_classroom_text))
+ .check(matches(isDisplayed()))
+ onView(withText(R.string.onboarding_learner_intro_practice_text))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ context.getString(
+ R.string.onboarding_learner_intro_feedback_text,
+ context.getString(R.string.app_name)
+ )
+ )
+ ).check(matches(isDisplayed()))
+ }
+ }
+
+ private fun launchOnboardingLearnerIntroActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ IntroActivity.createIntroActivity(
+ context,
+ testProfileNickname
+ )
+ )
+ 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 = [
+ 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, 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,
+ 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
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(introFragmentTest: IntroFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerIntroFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(introFragmentTest: IntroFragmentTest) {
+ component.inject(introFragmentTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
+}
diff --git a/model/src/main/proto/arguments.proto b/model/src/main/proto/arguments.proto
index 2aca58d5e01..3784edb8b83 100644
--- a/model/src/main/proto/arguments.proto
+++ b/model/src/main/proto/arguments.proto
@@ -835,3 +835,9 @@ message WalkthroughFinalFragmentArguments {
// The ID of the topic to which the opening exploration belongs.
string topic_id = 1;
}
+
+// Params required when creating a new IntroActivity.
+message IntroActivityParams {
+ // The nickname associated with a newly created profile.
+ string profile_nickname = 1;
+}
diff --git a/model/src/main/proto/screens.proto b/model/src/main/proto/screens.proto
index 48631a4a3e4..2824e72bd6c 100644
--- a/model/src/main/proto/screens.proto
+++ b/model/src/main/proto/screens.proto
@@ -167,6 +167,9 @@ enum ScreenName {
// Screen name value for the scenario when the create new learner profile activity is visible to the user.
CREATE_PROFILE_ACTIVITY = 52;
+
+ // Screen name value for the scenario when the learner welcome activity is visible to the user.
+ INTRO_ACTIVITY = 53;
}
// Defines the current visible UI screen of the application.
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 107658a4fcc..02da7bda12d 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -1085,6 +1085,13 @@ test_file_exemption {
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt"
test_file_not_required: true
+}test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt"
+ test_file_not_required: true
}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt"
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 24f5db6dabf..96c5899da27 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
@@ -857,6 +857,7 @@ class EventBundleCreator @Inject constructor(
ScreenName.CLASSROOM_LIST_ACTIVITY -> "classroom_list_activity"
ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY -> "onboarding_profile_type_activity"
ScreenName.CREATE_PROFILE_ACTIVITY -> "create_profile_activity"
+ ScreenName.INTRO_ACTIVITY -> "intro_activity"
}
private fun AppLanguageSelection.toAnalyticsText(): String {
From b5354f9cdfcb77fa539058a118c5e39b4686bc27 Mon Sep 17 00:00:00 2001
From: Vishwajith Shettigar
<76042077+Vishwajith-Shettigar@users.noreply.github.com>
Date: Mon, 1 Jul 2024 16:48:59 +0530
Subject: [PATCH 4/6] Fix #5069: Add a "hint/solution viewed" event to
complement the existing "offered"/"unlocked" events. (#5298)
## Explanation
Fixes #5069: Added a new event called view existing for hints and
solution. This event logs every time user views a hint or solution,
except for the first time.
Existing event log access_hint and access_solution renamed to
reveal_hint and reveal_solution.
### Hints view log
[hint.webm](https://github.com/oppia/oppia-android/assets/76042077/c7ab6b74-f5a9-46d9-a8fa-cf1c38fc9985)
### Solution view log
[solution.webm](https://github.com/oppia/oppia-android/assets/76042077/e868e261-ef99-4742-a938-6a15a4a51723)
## Essential Checklist
- [ ] 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: ...".)
- [ ] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [ ] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [ ] 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)).
- [ ] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [ ] 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>
---
...HintsAndSolutionDialogFragmentPresenter.kt | 8 +
.../app/hintsandsolution/ViewHintListener.kt | 10 ++
.../hintsandsolution/ViewSolutionInterface.kt | 9 +
.../player/exploration/ExplorationActivity.kt | 12 ++
.../ExplorationActivityPresenter.kt | 16 ++
.../player/exploration/ExplorationFragment.kt | 6 +
.../ExplorationFragmentPresenter.kt | 7 +
.../android/app/player/state/StateFragment.kt | 8 +
.../player/state/StateFragmentPresenter.kt | 8 +
.../ExplorationProgressController.kt | 128 ++++++++++++++-
.../analytics/LearnerAnalyticsLogger.kt | 22 ++-
.../ExplorationProgressControllerTest.kt | 83 +++++++++-
.../analytics/LearnerAnalyticsLoggerTest.kt | 89 ++++++++--
model/src/main/proto/oppia_logger.proto | 14 +-
scripts/assets/test_file_exemptions.textproto | 8 +
.../testing/logging/EventLogSubject.kt | 98 ++++++++---
.../util/logging/EventBundleCreator.kt | 13 +-
...entTypeToHumanReadableNameConverterImpl.kt | 6 +-
...entTypeToHumanReadableNameConverterImpl.kt | 6 +-
.../util/logging/EventBundleCreatorTest.kt | 143 ++++++++++++++--
.../KenyaAlphaEventBundleCreatorTest.kt | 155 +++++++++++++++---
21 files changed, 745 insertions(+), 104 deletions(-)
create mode 100644 app/src/main/java/org/oppia/android/app/hintsandsolution/ViewHintListener.kt
create mode 100644 app/src/main/java/org/oppia/android/app/hintsandsolution/ViewSolutionInterface.kt
diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt
index 46d7c3fd9c7..16f9dec7b19 100644
--- a/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/HintsAndSolutionDialogFragmentPresenter.kt
@@ -185,11 +185,15 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor(
binding.expandableHintHeader.setOnClickListener {
if (hintViewModel.isHintRevealed.get()) {
expandOrCollapseItem(position)
+ if (position in expandedItemIndexes)
+ (fragment.requireActivity() as? ViewHintListener)?.viewHint(hintIndex = position)
}
}
binding.expandHintListIcon.setOnClickListener {
if (hintViewModel.isHintRevealed.get()) {
expandOrCollapseItem(position)
+ if (position in expandedItemIndexes)
+ (fragment.requireActivity() as? ViewHintListener)?.viewHint(hintIndex = position)
}
}
@@ -262,11 +266,15 @@ class HintsAndSolutionDialogFragmentPresenter @Inject constructor(
binding.expandableSolutionHeader.setOnClickListener {
if (solutionViewModel.isSolutionRevealed.get()) {
expandOrCollapseItem(position)
+ if (position in expandedItemIndexes)
+ (fragment.requireActivity() as? ViewSolutionInterface)?.viewSolution()
}
}
binding.expandSolutionListIcon.setOnClickListener {
if (solutionViewModel.isSolutionRevealed.get()) {
expandOrCollapseItem(position)
+ if (position in expandedItemIndexes)
+ (fragment.requireActivity() as? ViewSolutionInterface)?.viewSolution()
}
}
diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/ViewHintListener.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/ViewHintListener.kt
new file mode 100644
index 00000000000..ef3965321ce
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/ViewHintListener.kt
@@ -0,0 +1,10 @@
+package org.oppia.android.app.hintsandsolution
+
+/** Callback listener for when the user wishes to view a hint. */
+interface ViewHintListener {
+ /**
+ * Called when the user indicates they want to view the hint corresponding to the specified
+ * index.
+ */
+ fun viewHint(hintIndex: Int)
+}
diff --git a/app/src/main/java/org/oppia/android/app/hintsandsolution/ViewSolutionInterface.kt b/app/src/main/java/org/oppia/android/app/hintsandsolution/ViewSolutionInterface.kt
new file mode 100644
index 00000000000..eb1fefd3f2b
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/hintsandsolution/ViewSolutionInterface.kt
@@ -0,0 +1,9 @@
+package org.oppia.android.app.hintsandsolution
+
+/** Interface to check the preference regarding alert for [HintsAndSolutionDialogFragment]. */
+interface ViewSolutionInterface {
+ /**
+ * Called when the user indicates they want to view the solution.
+ */
+ fun viewSolution()
+}
diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt
index c657a4a35fb..4e73ac992e3 100755
--- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivity.kt
@@ -9,6 +9,8 @@ import org.oppia.android.app.hintsandsolution.HintsAndSolutionDialogFragment
import org.oppia.android.app.hintsandsolution.HintsAndSolutionListener
import org.oppia.android.app.hintsandsolution.RevealHintListener
import org.oppia.android.app.hintsandsolution.RevealSolutionInterface
+import org.oppia.android.app.hintsandsolution.ViewHintListener
+import org.oppia.android.app.hintsandsolution.ViewSolutionInterface
import org.oppia.android.app.model.ExplorationActivityParams
import org.oppia.android.app.model.HelpIndex
import org.oppia.android.app.model.ProfileId
@@ -37,7 +39,9 @@ class ExplorationActivity :
HintsAndSolutionListener,
RouteToHintsAndSolutionListener,
RevealHintListener,
+ ViewHintListener,
RevealSolutionInterface,
+ ViewSolutionInterface,
DefaultFontSizeStateListener,
HintsAndSolutionExplorationManagerListener,
ConceptCardListener,
@@ -188,4 +192,12 @@ class ExplorationActivity :
override fun requestVoiceOverIconSpotlight(numberOfLogins: Int) {
explorationActivityPresenter.requestVoiceOverIconSpotlight(numberOfLogins)
}
+
+ override fun viewHint(hintIndex: Int) {
+ explorationActivityPresenter.viewHint(hintIndex)
+ }
+
+ override fun viewSolution() {
+ explorationActivityPresenter.viewSolution()
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt
index e940dabb48c..76812ff7522 100644
--- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt
@@ -401,6 +401,14 @@ class ExplorationActivityPresenter @Inject constructor(
explorationFragment.revealHint(hintIndex)
}
+ fun viewHint(hintIndex: Int) {
+ val explorationFragment =
+ activity.supportFragmentManager.findFragmentByTag(
+ TAG_EXPLORATION_FRAGMENT
+ ) as ExplorationFragment
+ explorationFragment.viewHint(hintIndex)
+ }
+
fun revealSolution() {
val explorationFragment =
activity.supportFragmentManager.findFragmentByTag(
@@ -409,6 +417,14 @@ class ExplorationActivityPresenter @Inject constructor(
explorationFragment.revealSolution()
}
+ fun viewSolution() {
+ val explorationFragment =
+ activity.supportFragmentManager.findFragmentByTag(
+ TAG_EXPLORATION_FRAGMENT
+ ) as ExplorationFragment
+ explorationFragment.viewSolution()
+ }
+
private fun showProgressDatabaseFullDialogFragment() {
val previousFragment = activity.supportFragmentManager.findFragmentByTag(
TAG_PROGRESS_DATABASE_FULL_DIALOG
diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt
index b72f2f0da61..8411ab49c46 100755
--- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragment.kt
@@ -72,9 +72,15 @@ class ExplorationFragment : InjectableFragment() {
explorationFragmentPresenter.revealHint(hintIndex)
}
+ fun viewHint(hintIndex: Int) {
+ explorationFragmentPresenter.viewHint(hintIndex)
+ }
fun revealSolution() {
explorationFragmentPresenter.revealSolution()
}
+ fun viewSolution() {
+ explorationFragmentPresenter.viewSolution()
+ }
fun dismissConceptCard() = explorationFragmentPresenter.dismissConceptCard()
diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt
index 8796b96b672..a64fa466a65 100755
--- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationFragmentPresenter.kt
@@ -129,11 +129,18 @@ class ExplorationFragmentPresenter @Inject constructor(
fun revealHint(hintIndex: Int) {
getStateFragment()?.revealHint(hintIndex)
}
+ fun viewHint(hintIndex: Int) {
+ getStateFragment()?.viewHint(hintIndex)
+ }
fun revealSolution() {
getStateFragment()?.revealSolution()
}
+ fun viewSolution() {
+ getStateFragment()?.viewSolution()
+ }
+
fun dismissConceptCard() = getStateFragment()?.dismissConceptCard()
fun getExplorationCheckpointState() = getStateFragment()?.getExplorationCheckpointState()
diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt
index ca7c1d55565..afd00a47f9b 100755
--- a/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt
@@ -141,8 +141,16 @@ class StateFragment :
stateFragmentPresenter.revealHint(hintIndex)
}
+ fun viewHint(hintIndex: Int) {
+ stateFragmentPresenter.viewHint(hintIndex)
+ }
+
fun revealSolution() = stateFragmentPresenter.revealSolution()
+ fun viewSolution() {
+ stateFragmentPresenter.viewSolution()
+ }
+
fun dismissConceptCard() = stateFragmentPresenter.dismissConceptCard()
fun getExplorationCheckpointState() = stateFragmentPresenter.getExplorationCheckpointState()
diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt
index aea996137c5..a6726774c96 100755
--- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt
@@ -268,10 +268,18 @@ class StateFragmentPresenter @Inject constructor(
subscribeToHintSolution(explorationProgressController.submitHintIsRevealed(hintIndex))
}
+ fun viewHint(hintIndex: Int) {
+ explorationProgressController.submitHintIsViewed(hintIndex)
+ }
+
fun revealSolution() {
subscribeToHintSolution(explorationProgressController.submitSolutionIsRevealed())
}
+ fun viewSolution() {
+ explorationProgressController.submitSolutionIsViewed()
+ }
+
private fun getAudioFragment(): Fragment? {
return fragment.childFragmentManager.findFragmentByTag(TAG_AUDIO_FRAGMENT)
}
diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt
index 22a64fdf412..3c9ef56020a 100644
--- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt
@@ -61,8 +61,12 @@ private const val SUBMIT_ANSWER_RESULT_PROVIDER_ID =
"ExplorationProgressController.submit_answer_result"
private const val SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID =
"ExplorationProgressController.submit_hint_revealed_result"
+private const val SUBMIT_HINT_VIEWED_RESULT_PROVIDER_ID =
+ "ExplorationProgressController.submit_hint_revealed_result"
private const val SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID =
"ExplorationProgressController.submit_solution_revealed_result"
+private const val SUBMIT_SOLUTION_VIEWED_RESULT_PROVIDER_ID =
+ "ExplorationProgressController.submit_solution_revealed_result"
private const val MOVE_TO_PREVIOUS_STATE_RESULT_PROVIDER_ID =
"ExplorationProgressController.move_to_previous_state_result"
private const val MOVE_TO_NEXT_STATE_RESULT_PROVIDER_ID =
@@ -275,6 +279,24 @@ class ExplorationProgressController @Inject constructor(
return submitResultFlow.convertToSessionProvider(SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID)
}
+ /**
+ * Notifies the controller that the user has viewed a hint.
+ *
+ * @param hintIndex index of the hint that is being viewed
+ *
+ * @return a [DataProvider] that indicates success/failure of the operation (the actual payload of
+ * the result isn't relevant)
+ */
+
+ fun submitHintIsViewed(hintIndex: Int): DataProvider {
+ val submitResultFlow = createAsyncResultStateFlow()
+ val message = ControllerMessage.LogHintIsViewed(hintIndex, activeSessionId, submitResultFlow)
+ sendCommandForOperation(message) {
+ "Failed to schedule command for viewing hint: $hintIndex."
+ }
+ return submitResultFlow.convertToSessionProvider(SUBMIT_HINT_VIEWED_RESULT_PROVIDER_ID)
+ }
+
/**
* Notifies the controller that the user has revealed the solution to the current state.
*
@@ -291,6 +313,18 @@ class ExplorationProgressController @Inject constructor(
return submitResultFlow.convertToSessionProvider(SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID)
}
+ /**
+ * Notifies the controller that the user has viewed the answer.
+ * @return a [DataProvider] that indicates success/failure of the operation (the actual payload of
+ * the result isn't relevant)
+ */
+ fun submitSolutionIsViewed(): DataProvider {
+ val submitResultFlow = createAsyncResultStateFlow()
+ val message = ControllerMessage.LogSolutionIsViewed(activeSessionId, submitResultFlow)
+ sendCommandForOperation(message) { "Failed to schedule command for viewing the solution." }
+ return submitResultFlow.convertToSessionProvider(SUBMIT_SOLUTION_VIEWED_RESULT_PROVIDER_ID)
+ }
+
/**
* Navigates to the previous state in the graph. If the learner is currently on the initial state,
* this method will throw an exception. Calling code is responsible for ensuring this method is
@@ -419,7 +453,6 @@ class ExplorationProgressController @Inject constructor(
@OptIn(ObsoleteCoroutinesApi::class)
private fun createControllerCommandActor(): SendChannel> {
lateinit var controllerState: ControllerState
-
// Use an unlimited capacity buffer so that commands can be sent asynchronously without blocking
// the main thread or scheduling an extra coroutine.
@Suppress("JoinDeclarationAndAssignment") // Warning is incorrect in this case.
@@ -491,8 +524,14 @@ class ExplorationProgressController @Inject constructor(
is ControllerMessage.HintIsRevealed -> {
controllerState.submitHintIsRevealedImpl(message.callbackFlow, message.hintIndex)
}
+ is ControllerMessage.LogHintIsViewed ->
+ controllerState.logViewedHintImpl(
+ activeSessionId, message.hintIndex, message.callbackFlow
+ )
is ControllerMessage.SolutionIsRevealed ->
controllerState.submitSolutionIsRevealedImpl(message.callbackFlow)
+ is ControllerMessage.LogSolutionIsViewed ->
+ controllerState.logViewedSolutionImpl(activeSessionId, message.callbackFlow)
is ControllerMessage.MoveToPreviousState ->
controllerState.moveToPreviousStateImpl(message.callbackFlow)
is ControllerMessage.MoveToNextState ->
@@ -790,6 +829,43 @@ class ExplorationProgressController @Inject constructor(
}
}
+ private suspend fun ControllerState.logViewedHintImpl(
+ sessionId: String,
+ hintIndex: Int,
+ submitLogHintViewedResultFlow: MutableStateFlow>
+ ) {
+ tryOperation(submitLogHintViewedResultFlow) {
+ check(explorationProgress.playStage != NOT_PLAYING) {
+ "Cannot log hint viewed if an exploration is not being played."
+ }
+ check(explorationProgress.playStage != LOADING_EXPLORATION) {
+ "Cannot log hint viewed if an exploration is being loaded."
+ }
+ check(explorationProgress.playStage != SUBMITTING_ANSWER) {
+ "Cannot log hint viewed if an answer submission is pending."
+ }
+ maybeLogViewedHint(sessionId, hintIndex)
+ }
+ }
+
+ private suspend fun ControllerState.logViewedSolutionImpl(
+ sessionId: String,
+ submitLogSolutionViewedResultFlow: MutableStateFlow>
+ ) {
+ tryOperation(submitLogSolutionViewedResultFlow) {
+ check(explorationProgress.playStage != NOT_PLAYING) {
+ "Cannot log solution viewed if an exploration is not being played."
+ }
+ check(explorationProgress.playStage != LOADING_EXPLORATION) {
+ "Cannot log solution viewed while the exploration is being loaded."
+ }
+ check(explorationProgress.playStage != SUBMITTING_ANSWER) {
+ "Cannot log solution viewed if an answer submission is pending."
+ }
+ maybeLogViewedSolution(sessionId)
+ }
+ }
+
private fun ControllerState.maybeLogUpdatedHelpIndex(
helpIndex: HelpIndex,
activeSessionId: String
@@ -800,6 +876,25 @@ class ExplorationProgressController @Inject constructor(
}
}
+ private fun ControllerState.maybeLogViewedHint(
+ activeSessionId: String,
+ hintIndex: Int
+ ) {
+ // Only log if the current session is active.
+ if (sessionId == activeSessionId) {
+ stateAnalyticsLogger?.logViewHint(hintIndex)
+ }
+ }
+
+ private fun ControllerState.maybeLogViewedSolution(
+ activeSessionId: String
+ ) {
+ // Only log if the current session is active.
+ if (sessionId == activeSessionId) {
+ stateAnalyticsLogger?.logViewSolution()
+ }
+ }
+
private suspend fun ControllerState.tryOperation(
resultFlow: MutableStateFlow>,
recomputeState: Boolean = true,
@@ -1216,12 +1311,12 @@ class ExplorationProgressController @Inject constructor(
NEXT_AVAILABLE_HINT_INDEX ->
stateAnalyticsLogger?.logHintUnlocked(newHelpIndex.nextAvailableHintIndex)
LATEST_REVEALED_HINT_INDEX ->
- stateAnalyticsLogger?.logViewHint(newHelpIndex.latestRevealedHintIndex)
+ stateAnalyticsLogger?.logRevealHint(newHelpIndex.latestRevealedHintIndex)
SHOW_SOLUTION -> stateAnalyticsLogger?.logSolutionUnlocked()
EVERYTHING_REVEALED -> when (helpIndex.indexTypeCase) {
- SHOW_SOLUTION -> stateAnalyticsLogger?.logViewSolution()
+ SHOW_SOLUTION -> stateAnalyticsLogger?.logRevealSolution()
NEXT_AVAILABLE_HINT_INDEX -> // No solution, so revealing the hint ends available help.
- stateAnalyticsLogger?.logViewHint(helpIndex.nextAvailableHintIndex)
+ stateAnalyticsLogger?.logRevealHint(helpIndex.nextAvailableHintIndex)
// Nothing to do in these cases.
LATEST_REVEALED_HINT_INDEX, EVERYTHING_REVEALED, INDEXTYPE_NOT_SET, null -> {}
}
@@ -1349,6 +1444,31 @@ class ExplorationProgressController @Inject constructor(
override val callbackFlow: MutableStateFlow>? = null
) : ControllerMessage()
+ /**
+ * [ControllerMessage] to log cases when the user has viewed a hint for the current session.
+ *
+ * Specific measures are taken to ensure that the handler for this message does not log the
+ * change if the current active session has changed (since that's generally indicative of an
+ * error--hints can't continue to change after the session has ended).
+ */
+ data class LogHintIsViewed(
+ val hintIndex: Int,
+ override val sessionId: String,
+ override val callbackFlow: MutableStateFlow>
+ ) : ControllerMessage()
+
+ /**
+ * [ControllerMessage] to log cases when the user has viewed the solution for the current
+ * session.
+ *
+ * Specific measures are taken to ensure that the handler for this message does not log the
+ * change if the current active session has changed.
+ */
+ data class LogSolutionIsViewed(
+ override val sessionId: String,
+ override val callbackFlow: MutableStateFlow>
+ ) : ControllerMessage()
+
/**
* [ControllerMessage] to ensure a successfully saved checkpoint is reflected in other parts of
* the app (e.g. that an exploration is considered 'in-progress' in such circumstances).
diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt
index f0fce6b94b0..99511b001ed 100644
--- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt
+++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLogger.kt
@@ -371,9 +371,17 @@ class LearnerAnalyticsLogger @Inject constructor(
logStateEvent(hintIndex, ::createHintContext, EventBuilder::setHintUnlockedContext)
}
- /** Logs that the hint corresponding to [hintIndex] has been viewed by the learner. */
+ /** Logs that the hint corresponding to [hintIndex] has been revealed by the learner. */
+ fun logRevealHint(hintIndex: Int) {
+ logStateEvent(hintIndex, ::createHintContext, EventBuilder::setRevealHintContext)
+ }
+
+ /**
+ * Logs the event indicating that the learner has viewed a hint corresponding to [hintIndex],
+ * excluding the first-time viewing.
+ */
fun logViewHint(hintIndex: Int) {
- logStateEvent(hintIndex, ::createHintContext, EventBuilder::setAccessHintContext)
+ logStateEvent(hintIndex, ::createHintContext, EventBuilder::setViewExistingHintContext)
}
/** Logs that the solution to the current card has been unlocked by the learner. */
@@ -382,8 +390,16 @@ class LearnerAnalyticsLogger @Inject constructor(
}
/** Logs that the solution to the current card has been viewed by the learner. */
+ fun logRevealSolution() {
+ logStateEvent(EventBuilder::setRevealSolutionContext)
+ }
+
+ /**
+ * Logs the event indicating that the learner has viewed the solution to the current card,
+ * excluding the first-time viewing.
+ */
fun logViewSolution() {
- logStateEvent(EventBuilder::setAccessSolutionContext)
+ logStateEvent(EventBuilder::setViewExistingSolutionContext)
}
/**
diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt
index a6b797562e4..97f81c0f3af 100644
--- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt
@@ -2466,7 +2466,7 @@ class ExplorationProgressControllerTest {
}
@Test
- fun testHint_offeredThenViewed_logsViewHintEvent_logsProgressSavingSuccessEvent() {
+ fun testHint_offeredThenViewed_logsRevealedHint_logsPgrssSavSuccEvent_logsExtingHintViwdEvent() {
logIntoAnalyticsReadyAdminProfile()
startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2)
waitForGetCurrentStateSuccessfulLoad()
@@ -2478,9 +2478,11 @@ class ExplorationProgressControllerTest {
monitorFactory.ensureDataProviderExecutes(
explorationProgressController.submitHintIsRevealed(hintIndex = 0)
)
-
- val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2)
- assertThat(eventLogList[0]).hasAccessHintContextThat {
+ monitorFactory.ensureDataProviderExecutes(
+ explorationProgressController.submitHintIsViewed(hintIndex = 0)
+ )
+ val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(3)
+ assertThat(eventLogList[0]).hasRevealHintContextThat {
hasExplorationDetailsThat().containsTestExp2Details()
hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Fractions")
hasHintIndexThat().isEqualTo(0)
@@ -2488,6 +2490,34 @@ class ExplorationProgressControllerTest {
assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat {
containsTestExp2Details()
}
+ assertThat(eventLogList[2]).hasViewExistingHintContextThat {
+ hasExplorationDetailsThat().containsTestExp2Details()
+ hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Fractions")
+ hasHintIndexThat().isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun testHint_existingHintViewed_logsExistingHintViewedEvent() {
+ logIntoAnalyticsReadyAdminProfile()
+ startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2)
+ waitForGetCurrentStateSuccessfulLoad()
+ playThroughPrototypeState1AndMoveToNextState()
+ // Submit 2 wrong answers to trigger a hint becoming available.
+ submitWrongAnswerForPrototypeState2()
+ submitWrongAnswerForPrototypeState2()
+
+ monitorFactory.ensureDataProviderExecutes(
+ explorationProgressController.submitHintIsViewed(hintIndex = 0)
+ )
+
+ val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
+
+ assertThat(eventLog).hasViewExistingHintContextThat {
+ hasExplorationDetailsThat().containsTestExp2Details()
+ hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Fractions")
+ hasHintIndexThat().isEqualTo(0)
+ }
}
@Test
@@ -2525,7 +2555,7 @@ class ExplorationProgressControllerTest {
}
@Test
- fun testHint_lastHintWithNoSol_offeredThenViewed_logsViewHintEvt_logsProgressSavingSuccessEvt() {
+ fun testHint_lastHintWithNoSol_offeredThenViewed_logsRevealedHintEvt_logsPgrssSavingSucssEvt() {
logIntoAnalyticsReadyAdminProfile()
startPlayingNewExploration(FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0)
waitForGetCurrentStateSuccessfulLoad()
@@ -2542,7 +2572,7 @@ class ExplorationProgressControllerTest {
)
val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2)
- assertThat(eventLogList[0]).hasAccessHintContextThat {
+ assertThat(eventLogList[0]).hasRevealHintContextThat {
hasExplorationDetailsThat().containsFractionsExp0Details()
hasExplorationDetailsThat().hasStateNameThat().isEqualTo("Parts of a whole")
hasHintIndexThat().isEqualTo(0)
@@ -2597,12 +2627,49 @@ class ExplorationProgressControllerTest {
explorationProgressController.submitSolutionIsRevealed()
)
- val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(2)
- assertThat(eventLogList[0]).hasAccessSolutionContextThat {
+ monitorFactory.ensureDataProviderExecutes(
+ explorationProgressController.submitSolutionIsViewed()
+ )
+
+ val eventLogList = fakeAnalyticsEventLogger.getMostRecentEvents(3)
+ assertThat(eventLogList[0]).hasRevealSolutionContextThat {
containsTestExp2Details()
hasStateNameThat().isEqualTo("Fractions")
}
assertThat(eventLogList[1]).hasProgressSavingSuccessContextThat().containsTestExp2Details()
+ assertThat(eventLogList[2]).hasViewExistingSolutionContextThat {
+ containsTestExp2Details()
+ hasStateNameThat().isEqualTo("Fractions")
+ }
+ }
+
+ @Test
+ fun testSolution_viewExistingSolution_logsExistingSolutionViewedEvent() {
+ logIntoAnalyticsReadyAdminProfile()
+ startPlayingNewExploration(TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2)
+ waitForGetCurrentStateSuccessfulLoad()
+ playThroughPrototypeState1AndMoveToNextState()
+ // Submit 2 wrong answers to trigger the hint.
+ submitWrongAnswerForPrototypeState2()
+ submitWrongAnswerForPrototypeState2()
+ monitorFactory.ensureDataProviderExecutes(
+ explorationProgressController.submitHintIsRevealed(hintIndex = 0)
+ )
+ // Submit another wrong answer to trigger the solution.
+ submitWrongAnswerForPrototypeState2()
+ testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10))
+
+ monitorFactory.ensureDataProviderExecutes(
+ explorationProgressController.submitSolutionIsRevealed()
+ )
+ monitorFactory.ensureDataProviderExecutes(
+ explorationProgressController.submitSolutionIsViewed()
+ )
+ val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(eventLog).hasViewExistingSolutionContextThat {
+ containsTestExp2Details()
+ hasStateNameThat().isEqualTo("Fractions")
+ }
}
@Test
diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt
index 2562ecbfca5..7b6f52d63af 100644
--- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/LearnerAnalyticsLoggerTest.kt
@@ -829,18 +829,18 @@ class LearnerAnalyticsLoggerTest {
}
@Test
- fun testStateAnalyticsLogger_logViewHint_logsStateEventWithHintIndex() {
+ fun testStateAnalyticsLogger_logRevealHint_logsStateEventWithHintIndex() {
val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
val expLogger = learnerAnalyticsLogger.beginExploration(exploration5)
val stateLogger = expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME))
testCoroutineDispatchers.runCurrent()
- stateLogger.logViewHint(hintIndex = 1)
+ stateLogger.logRevealHint(hintIndex = 1)
testCoroutineDispatchers.runCurrent()
val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
assertThat(eventLog).isEssentialPriority()
- assertThat(eventLog).hasAccessHintContextThat {
+ assertThat(eventLog).hasRevealHintContextThat {
hasExplorationDetailsThat {
hasTopicIdThat().isEqualTo(TEST_TOPIC_ID)
hasStoryIdThat().isEqualTo(TEST_STORY_ID)
@@ -858,18 +858,47 @@ class LearnerAnalyticsLoggerTest {
}
@Test
- fun testStateAnalyticsLogger_logViewHint_diffIndex_logsStateEventWithHintIndex() {
+ fun testStateAnalyticsLogger_logRevealHint_diffIndex_logsStateEventWithHintIndex() {
val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
val expLogger = learnerAnalyticsLogger.beginExploration(exploration5)
val stateLogger = expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME))
testCoroutineDispatchers.runCurrent()
- stateLogger.logViewHint(hintIndex = 2)
+ stateLogger.logRevealHint(hintIndex = 2)
testCoroutineDispatchers.runCurrent()
val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
assertThat(eventLog).isEssentialPriority()
- assertThat(eventLog).hasAccessHintContextThat().hasHintIndexThat().isEqualTo(2)
+ assertThat(eventLog).hasRevealHintContextThat().hasHintIndexThat().isEqualTo(2)
+ }
+
+ @Test
+ fun testStateAnalyticsLogger_logViewHint_logsStateEventWithHintIndex() {
+ val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
+ val expLogger = learnerAnalyticsLogger.beginExploration(exploration5)
+ val stateLogger = expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME))
+ testCoroutineDispatchers.runCurrent()
+
+ stateLogger.logViewHint(hintIndex = 1)
+ testCoroutineDispatchers.runCurrent()
+
+ val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(eventLog).isEssentialPriority()
+ assertThat(eventLog).hasViewExistingHintContextThat {
+ hasExplorationDetailsThat {
+ hasTopicIdThat().isEqualTo(TEST_TOPIC_ID)
+ hasStoryIdThat().isEqualTo(TEST_STORY_ID)
+ hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5)
+ hasSessionIdThat().isEqualTo(DEFAULT_INITIAL_SESSION_ID)
+ hasVersionThat().isEqualTo(5)
+ hasStateNameThat().isEqualTo(TEST_EXP_5_STATE_THREE_NAME)
+ hasLearnerDetailsThat {
+ hasLearnerIdThat().isEqualTo(TEST_LEARNER_ID)
+ hasInstallationIdThat().isEqualTo(TEST_INSTALL_ID)
+ }
+ }
+ hasHintIndexThat().isEqualTo(1)
+ }
}
@Test
@@ -898,6 +927,32 @@ class LearnerAnalyticsLoggerTest {
}
}
+ @Test
+ fun testStateAnalyticsLogger_logRevealSolution_logsStateEvent() {
+ val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
+ val expLogger = learnerAnalyticsLogger.beginExploration(exploration5)
+ val stateLogger = expLogger.startCard(exploration5.getStateByName(TEST_EXP_5_STATE_THREE_NAME))
+ testCoroutineDispatchers.runCurrent()
+
+ stateLogger.logRevealSolution()
+ testCoroutineDispatchers.runCurrent()
+
+ val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(eventLog).isEssentialPriority()
+ assertThat(eventLog).hasRevealSolutionContextThat {
+ hasTopicIdThat().isEqualTo(TEST_TOPIC_ID)
+ hasStoryIdThat().isEqualTo(TEST_STORY_ID)
+ hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5)
+ hasSessionIdThat().isEqualTo(DEFAULT_INITIAL_SESSION_ID)
+ hasVersionThat().isEqualTo(5)
+ hasStateNameThat().isEqualTo(TEST_EXP_5_STATE_THREE_NAME)
+ hasLearnerDetailsThat {
+ hasLearnerIdThat().isEqualTo(TEST_LEARNER_ID)
+ hasInstallationIdThat().isEqualTo(TEST_INSTALL_ID)
+ }
+ }
+ }
+
@Test
fun testStateAnalyticsLogger_logViewSolution_logsStateEvent() {
val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
@@ -910,7 +965,7 @@ class LearnerAnalyticsLoggerTest {
val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
assertThat(eventLog).isEssentialPriority()
- assertThat(eventLog).hasAccessSolutionContextThat {
+ assertThat(eventLog).hasViewExistingSolutionContextThat() {
hasTopicIdThat().isEqualTo(TEST_TOPIC_ID)
hasStoryIdThat().isEqualTo(TEST_STORY_ID)
hasExplorationIdThat().isEqualTo(TEST_EXPLORATION_ID_5)
@@ -1398,7 +1453,7 @@ class LearnerAnalyticsLoggerTest {
@Test
@Iteration("no_install_id", "lid=learn", "iid=null", "elid=learn", "eid=")
@Iteration("no_learner_id", "lid=null", "iid=install", "elid=", "eid=install")
- fun testStateAnalyticsLogger_logViewHint_missingOneId_logsEventWithMissingId() {
+ fun testStateAnalyticsLogger_logRevealHint_missingOneId_logsEventWithMissingId() {
val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
val expLogger =
learnerAnalyticsLogger.beginExploration(
@@ -1407,11 +1462,11 @@ class LearnerAnalyticsLoggerTest {
testCoroutineDispatchers.runCurrent()
val stateLogger = expLogger.startCard(exploration5.getStateByName(exploration5.initStateName))
- stateLogger.logViewHint(hintIndex = 1)
+ stateLogger.logRevealHint(hintIndex = 1)
testCoroutineDispatchers.runCurrent()
val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
- assertThat(eventLog).hasAccessHintContextThat {
+ assertThat(eventLog).hasRevealHintContextThat {
hasExplorationDetailsThat {
hasLearnerDetailsThat {
hasLearnerIdThat().isEqualTo(expectedLearnerIdParameter)
@@ -1422,14 +1477,14 @@ class LearnerAnalyticsLoggerTest {
}
@Test
- fun testStateAnalyticsLogger_logViewHint_noInstallOrLearnerIds_logsEventAndConsoleErrors() {
+ fun testStateAnalyticsLogger_logRevealHint_noInstallOrLearnerIds_logsEventAndConsoleErrors() {
val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
val expLogger =
learnerAnalyticsLogger.beginExploration(exploration5, learnerId = null, installationId = null)
testCoroutineDispatchers.runCurrent()
val stateLogger = expLogger.startCard(exploration5.getStateByName(exploration5.initStateName))
- stateLogger.logViewHint(hintIndex = 1)
+ stateLogger.logRevealHint(hintIndex = 1)
testCoroutineDispatchers.runCurrent()
// See testExpLogger_logExitExploration_noInstallOrLearnerIds_logsEventAndConsoleErrors.
@@ -1486,7 +1541,7 @@ class LearnerAnalyticsLoggerTest {
@Test
@Iteration("no_install_id", "lid=learn", "iid=null", "elid=learn", "eid=")
@Iteration("no_learner_id", "lid=null", "iid=install", "elid=", "eid=install")
- fun testStateAnalyticsLogger_logViewSolution_missingOneId_logsEventWithMissingId() {
+ fun testStateAnalyticsLogger_logRevealSolution_missingOneId_logsEventWithMissingId() {
val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
val expLogger =
learnerAnalyticsLogger.beginExploration(
@@ -1495,11 +1550,11 @@ class LearnerAnalyticsLoggerTest {
testCoroutineDispatchers.runCurrent()
val stateLogger = expLogger.startCard(exploration5.getStateByName(exploration5.initStateName))
- stateLogger.logViewSolution()
+ stateLogger.logRevealSolution()
testCoroutineDispatchers.runCurrent()
val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
- assertThat(eventLog).hasAccessSolutionContextThat {
+ assertThat(eventLog).hasRevealSolutionContextThat {
hasLearnerDetailsThat {
hasLearnerIdThat().isEqualTo(expectedLearnerIdParameter)
hasInstallationIdThat().isEqualTo(expectedInstallIdParameter)
@@ -1508,14 +1563,14 @@ class LearnerAnalyticsLoggerTest {
}
@Test
- fun testStateAnalyticsLogger_logViewSolution_noInstallOrLearnerIds_logsEventAndConsoleErrors() {
+ fun testStateAnalyticsLogger_logRevealSolution_noInstallOrLearnerIds_logsEventAndConsoleErrors() {
val exploration5 = loadExploration(TEST_EXPLORATION_ID_5)
val expLogger =
learnerAnalyticsLogger.beginExploration(exploration5, learnerId = null, installationId = null)
testCoroutineDispatchers.runCurrent()
val stateLogger = expLogger.startCard(exploration5.getStateByName(exploration5.initStateName))
- stateLogger.logViewSolution()
+ stateLogger.logRevealSolution()
testCoroutineDispatchers.runCurrent()
// See testExpLogger_logExitExploration_noInstallOrLearnerIds_logsEventAndConsoleErrors.
diff --git a/model/src/main/proto/oppia_logger.proto b/model/src/main/proto/oppia_logger.proto
index 07411ad365b..5ab3c9d32ff 100644
--- a/model/src/main/proto/oppia_logger.proto
+++ b/model/src/main/proto/oppia_logger.proto
@@ -102,14 +102,14 @@ message EventLog {
// The event being logged is related to offering a hint when it becomes available.
HintContext hint_unlocked_context = 18;
- // The event being logged is related to accessing a hint.
- HintContext access_hint_context = 19;
+ // The event being logged is related to revealing a hint.
+ HintContext reveal_hint_context = 19;
// The event being logged is related to offering a solution when it becomes available.
ExplorationContext solution_unlocked_context = 20;
// The event being logged is related to accessing a solution.
- ExplorationContext access_solution_context = 21;
+ ExplorationContext reveal_solution_context = 21;
// The event being logged is related to submitting an answer.
SubmitAnswerContext submit_answer_context = 22;
@@ -213,6 +213,12 @@ message EventLog {
// The event being logged is related to incorrect answer submission after returning back to
// the lesson.
ExplorationContext resume_lesson_submit_incorrect_answer_context = 53;
+
+ // The event being logged is related to viewing a hint that was already unlocked.
+ HintContext view_existing_hint_context = 54;
+
+ // The event being logged is related to viewing a solution that was already unlocked.
+ ExplorationContext view_existing_solution_context = 55;
}
}
@@ -266,7 +272,7 @@ message EventLog {
// Defined attributes that are common among other exploration related event log contexts.
ExplorationContext exploration_details = 1;
- // Corresponds to the index of the index being shown or last seen.
+ // Corresponds to the index of the hint being shown or last seen.
int32 hint_index = 4;
reserved 2;
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 02da7bda12d..d20ba9e9482 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -846,6 +846,14 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/hintsandsolution/SolutionViewModel.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path:"app/src/main/java/org/oppia/android/app/hintsandsolution/ViewSolutionInterface.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path:"app/src/main/java/org/oppia/android/app/hintsandsolution/ViewHintListener.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/home/HomeActivityPresenter.kt"
test_file_not_required: true
diff --git a/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt b/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt
index 3009fe88a79..62beee0a815 100644
--- a/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt
+++ b/testing/src/main/java/org/oppia/android/testing/logging/EventLogSubject.kt
@@ -16,8 +16,6 @@ import org.oppia.android.app.model.AppLanguageSelection.SelectionTypeCase.USE_SY
import org.oppia.android.app.model.AudioTranslationLanguageSelection
import org.oppia.android.app.model.EventLog
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ABANDON_SURVEY
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_HINT_CONTEXT
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_BACKGROUND_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_FOREGROUND_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.BEGIN_SURVEY
@@ -50,12 +48,16 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REACH_IN
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_CORRECT_ANSWER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SHOW_SURVEY_POPUP
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION_UNLOCKED_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_OVER_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SUBMIT_ANSWER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SWITCH_IN_LESSON_LANGUAGE
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.FeatureFlagItemContext
import org.oppia.android.app.model.MarketFitAnswer
import org.oppia.android.app.model.OppiaLanguage
@@ -505,29 +507,55 @@ class EventLogSubject private constructor(
}
/**
- * Verifies that the [EventLog] under test has a context corresponding to [ACCESS_HINT_CONTEXT]
+ * Verifies that the [EventLog] under test has a context corresponding to [REVEAL_HINT_CONTEXT]
* (per [EventLog.Context.getActivityContextCase]).
*/
- fun hasAccessHintContext() {
- assertThat(actual.context.activityContextCase).isEqualTo(ACCESS_HINT_CONTEXT)
+ fun hasRevealHintContext() {
+ assertThat(actual.context.activityContextCase).isEqualTo(REVEAL_HINT_CONTEXT)
}
/**
- * Verifies the [EventLog]'s context per [hasAccessHintContext] and returns a [HintContextSubject]
+ * Verifies the [EventLog]'s context per [hasRevealHintContext] and returns a [HintContextSubject]
* to test the corresponding context.
*/
- fun hasAccessHintContextThat(): HintContextSubject {
- hasAccessHintContext()
- return HintContextSubject.assertThat(actual.context.accessHintContext)
+ fun hasRevealHintContextThat(): HintContextSubject {
+ hasRevealHintContext()
+ return HintContextSubject.assertThat(actual.context.revealHintContext)
}
/**
* Verifies the [EventLog]'s context and executes [block] in the same way as
* [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by,
- * [hasAccessHintContextThat].
+ * [hasRevealHintContextThat].
*/
- fun hasAccessHintContextThat(block: HintContextSubject.() -> Unit) {
- hasAccessHintContextThat().block()
+ fun hasRevealHintContextThat(block: HintContextSubject.() -> Unit) {
+ hasRevealHintContextThat().block()
+ }
+
+ /**
+ * Verifies that the [EventLog] under test has a context corresponding to [VIEW_EXISTING_HINT_CONTEXT]
+ * (per [EventLog.Context.getActivityContextCase]).
+ */
+ fun hasViewExistingHintContext() {
+ assertThat(actual.context.activityContextCase).isEqualTo(VIEW_EXISTING_HINT_CONTEXT)
+ }
+
+ /**
+ * Verifies the [EventLog]'s context per [hasViewExistingHintContext] and returns a [HintContextSubject]
+ * to test the corresponding context.
+ */
+ fun hasViewExistingHintContextThat(): HintContextSubject {
+ hasViewExistingHintContext()
+ return HintContextSubject.assertThat(actual.context.viewExistingHintContext)
+ }
+
+ /**
+ * Verifies the [EventLog]'s context and executes [block] in the same way as
+ * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by,
+ * [hasViewExistingHintContextThat].
+ */
+ fun hasViewExistingHintContextThat(block: HintContextSubject.() -> Unit) {
+ hasViewExistingHintContextThat().block()
}
/**
@@ -558,28 +586,54 @@ class EventLogSubject private constructor(
/**
* Verifies that the [EventLog] under test has a context corresponding to
- * [ACCESS_SOLUTION_CONTEXT] (per [EventLog.Context.getActivityContextCase]).
+ * [REVEAL_SOLUTION_CONTEXT] (per [EventLog.Context.getActivityContextCase]).
+ */
+ fun hasRevealSolutionContext() {
+ assertThat(actual.context.activityContextCase).isEqualTo(REVEAL_SOLUTION_CONTEXT)
+ }
+
+ /**
+ * Verifies the [EventLog]'s context per [hasRevealSolutionContext] and returns an
+ * [ExplorationContextSubject] to test the corresponding context.
+ */
+ fun hasRevealSolutionContextThat(): ExplorationContextSubject {
+ hasRevealSolutionContext()
+ return ExplorationContextSubject.assertThat(actual.context.revealSolutionContext)
+ }
+
+ /**
+ * Verifies the [EventLog]'s context and executes [block] in the same way as
+ * [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by,
+ * [hasRevealSolutionContextThat].
+ */
+ fun hasRevealSolutionContextThat(block: ExplorationContextSubject.() -> Unit) {
+ hasRevealSolutionContextThat().block()
+ }
+
+ /**
+ * Verifies that the [EventLog] under test has a context corresponding to
+ * [VIEW_EXISTING_SOLUTION_CONTEXT] (per [EventLog.Context.getActivityContextCase]).
*/
- fun hasAccessSolutionContext() {
- assertThat(actual.context.activityContextCase).isEqualTo(ACCESS_SOLUTION_CONTEXT)
+ fun hasViewExistingSolutionContext() {
+ assertThat(actual.context.activityContextCase).isEqualTo(VIEW_EXISTING_SOLUTION_CONTEXT)
}
/**
- * Verifies the [EventLog]'s context per [hasAccessSolutionContext] and returns an
+ * Verifies the [EventLog]'s context per [hasViewExistingSolutionContext] and returns an
* [ExplorationContextSubject] to test the corresponding context.
*/
- fun hasAccessSolutionContextThat(): ExplorationContextSubject {
- hasAccessSolutionContext()
- return ExplorationContextSubject.assertThat(actual.context.accessSolutionContext)
+ fun hasViewExistingSolutionContextThat(): ExplorationContextSubject {
+ hasViewExistingSolutionContext()
+ return ExplorationContextSubject.assertThat(actual.context.viewExistingSolutionContext)
}
/**
* Verifies the [EventLog]'s context and executes [block] in the same way as
* [hasOpenExplorationActivityContextThat] except for the conditions of, and subject returned by,
- * [hasAccessSolutionContextThat].
+ * [hasViewExistingSolutionContextThat].
*/
- fun hasAccessSolutionContextThat(block: ExplorationContextSubject.() -> Unit) {
- hasAccessSolutionContextThat().block()
+ fun hasViewExistingSolutionContextThat(block: ExplorationContextSubject.() -> Unit) {
+ hasViewExistingSolutionContextThat().block()
}
/**
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 96c5899da27..b833b340206 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
@@ -8,8 +8,6 @@ import org.oppia.android.app.model.AppLanguageSelection
import org.oppia.android.app.model.AudioTranslationLanguageSelection
import org.oppia.android.app.model.EventLog
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ABANDON_SURVEY
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_HINT_CONTEXT
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACTIVITYCONTEXT_NOT_SET
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_BACKGROUND_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_FOREGROUND_CONTEXT
@@ -49,12 +47,16 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_L
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_LESSON_SUBMIT_INCORRECT_ANSWER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RETROFIT_CALL_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RETROFIT_CALL_FAILED_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SHOW_SURVEY_POPUP
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION_UNLOCKED_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_OVER_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SUBMIT_ANSWER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SWITCH_IN_LESSON_LANGUAGE
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.SwitchInLessonLanguageEventContext
import org.oppia.android.app.model.OppiaLanguage
import org.oppia.android.app.model.OppiaMetricLog
@@ -229,9 +231,12 @@ class EventBundleCreator @Inject constructor(
START_CARD_CONTEXT -> CardContext(activityName, startCardContext)
END_CARD_CONTEXT -> CardContext(activityName, endCardContext)
HINT_UNLOCKED_CONTEXT -> HintContext(activityName, hintUnlockedContext)
- ACCESS_HINT_CONTEXT -> HintContext(activityName, accessHintContext)
+ REVEAL_HINT_CONTEXT -> HintContext(activityName, revealHintContext)
+ VIEW_EXISTING_HINT_CONTEXT -> HintContext(activityName, viewExistingHintContext)
+ VIEW_EXISTING_SOLUTION_CONTEXT ->
+ ExplorationContext(activityName, viewExistingSolutionContext)
SOLUTION_UNLOCKED_CONTEXT -> ExplorationContext(activityName, solutionUnlockedContext)
- ACCESS_SOLUTION_CONTEXT -> ExplorationContext(activityName, accessSolutionContext)
+ REVEAL_SOLUTION_CONTEXT -> ExplorationContext(activityName, revealSolutionContext)
SUBMIT_ANSWER_CONTEXT -> SubmitAnswerContext(activityName, submitAnswerContext)
PLAY_VOICE_OVER_CONTEXT -> VoiceoverActionContext(activityName, playVoiceOverContext)
PAUSE_VOICE_OVER_CONTEXT -> VoiceoverActionContext(activityName, pauseVoiceOverContext)
diff --git a/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt
index 342333d65eb..da3ad145a46 100644
--- a/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt
+++ b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt
@@ -28,9 +28,11 @@ class KenyaAlphaEventTypeToHumanReadableNameConverterImpl @Inject constructor()
ActivityContextCase.START_CARD_CONTEXT -> "start_card_context"
ActivityContextCase.END_CARD_CONTEXT -> "end_card_context"
ActivityContextCase.HINT_UNLOCKED_CONTEXT -> "hint_offered_context"
- ActivityContextCase.ACCESS_HINT_CONTEXT -> "access_hint_context"
+ ActivityContextCase.REVEAL_HINT_CONTEXT -> "reveal_hint_context"
+ ActivityContextCase.VIEW_EXISTING_HINT_CONTEXT -> "view_existing_hint_context"
ActivityContextCase.SOLUTION_UNLOCKED_CONTEXT -> "solution_offered_context"
- ActivityContextCase.ACCESS_SOLUTION_CONTEXT -> "access_solution_context"
+ ActivityContextCase.REVEAL_SOLUTION_CONTEXT -> "reveal_solution_context"
+ ActivityContextCase.VIEW_EXISTING_SOLUTION_CONTEXT -> "view_existing_solution_context"
ActivityContextCase.SUBMIT_ANSWER_CONTEXT -> "submit_answer_context"
ActivityContextCase.PLAY_VOICE_OVER_CONTEXT -> "play_voice_over_context"
ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT -> "pause_voice_over_context"
diff --git a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt
index 55097655e74..2c4e72c10bf 100644
--- a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt
+++ b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt
@@ -38,9 +38,11 @@ class StandardEventTypeToHumanReadableNameConverterImpl @Inject constructor() :
ActivityContextCase.START_CARD_CONTEXT -> "start_exploration_card"
ActivityContextCase.END_CARD_CONTEXT -> "end_exploration_card"
ActivityContextCase.HINT_UNLOCKED_CONTEXT -> "unlock_hint"
- ActivityContextCase.ACCESS_HINT_CONTEXT -> "reveal_hint"
+ ActivityContextCase.REVEAL_HINT_CONTEXT -> "reveal_hint"
+ ActivityContextCase.VIEW_EXISTING_HINT_CONTEXT -> "view_existing_hint"
ActivityContextCase.SOLUTION_UNLOCKED_CONTEXT -> "unlock_solution"
- ActivityContextCase.ACCESS_SOLUTION_CONTEXT -> "reveal_solution"
+ ActivityContextCase.REVEAL_SOLUTION_CONTEXT -> "reveal_solution"
+ ActivityContextCase.VIEW_EXISTING_SOLUTION_CONTEXT -> "view_existing_solution"
ActivityContextCase.SUBMIT_ANSWER_CONTEXT -> "submit_answer"
ActivityContextCase.PLAY_VOICE_OVER_CONTEXT -> "click_play_voiceover_button"
ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT -> "click_pause_voiceover_button"
diff --git a/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt b/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt
index 8cab8e1778d..92c45550bd0 100644
--- a/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/logging/EventBundleCreatorTest.kt
@@ -20,8 +20,6 @@ import org.oppia.android.app.model.AudioTranslationLanguageSelection
import org.oppia.android.app.model.EventLog
import org.oppia.android.app.model.EventLog.CardContext
import org.oppia.android.app.model.EventLog.ConceptCardContext
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_HINT_CONTEXT
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACTIVITYCONTEXT_NOT_SET
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_BACKGROUND_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_FOREGROUND_CONTEXT
@@ -47,11 +45,15 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PAUSE_VO
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PLAY_VOICE_OVER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REACH_INVESTED_ENGAGEMENT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_EXPLORATION_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION_UNLOCKED_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_OVER_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SUBMIT_ANSWER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SWITCH_IN_LESSON_LANGUAGE
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.ExplorationContext
import org.oppia.android.app.model.EventLog.HintContext
import org.oppia.android.app.model.EventLog.LearnerDetailsContext
@@ -1099,18 +1101,18 @@ class EventBundleCreatorTest {
}
@Test
- fun testFillEventBundle_accessHintContextEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
+ fun testFillEventBundle_revealHintContextEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessHintContext())
+ val eventLog = createEventLog(context = createRevealHintContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
assertThat(typeName).isEqualTo("reveal_hint")
assertThat(bundle).hasSize(18)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_HINT_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_HINT_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -1124,18 +1126,70 @@ class EventBundleCreatorTest {
}
@Test
- fun testFillEventBundle_accessHintContextEvent_studyOn_fillsAllFieldsAndReturnsName() {
+ fun testFillEventBundle_revealHintContextEvent_studyOn_fillsAllFieldsAndReturnsName() {
setUpTestApplicationComponentWithLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessHintContext())
+ val eventLog = createEventLog(context = createRevealHintContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
assertThat(typeName).isEqualTo("reveal_hint")
assertThat(bundle).hasSize(20)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_HINT_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_HINT_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("ed_topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("ed_story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("ed_exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("ed_session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("ed_exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("ed_state_name").isEqualTo(TEST_STATE_NAME)
+ assertThat(bundle).string("hint_index").isEqualTo(TEST_HINT_INDEX_STR)
+ assertThat(bundle).string("ed_ld_learner_id").isEqualTo(TEST_LEARNER_ID)
+ assertThat(bundle).string("ed_ld_install_id").isEqualTo(TEST_INSTALLATION_ID)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingHintEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
+ setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingHintContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_hint")
+ assertThat(bundle).hasSize(18)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_HINT_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("ed_topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("ed_story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("ed_exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("ed_session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("ed_exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("ed_state_name").isEqualTo(TEST_STATE_NAME)
+ assertThat(bundle).string("hint_index").isEqualTo(TEST_HINT_INDEX_STR)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingHintEvent_studyOn_fillsAllFieldsAndReturnsName() {
+ setUpTestApplicationComponentWithLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingHintContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_hint")
+ assertThat(bundle).hasSize(20)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_HINT_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -1205,14 +1259,14 @@ class EventBundleCreatorTest {
setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessSolutionContext())
+ val eventLog = createEventLog(context = createRevealSolutionContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
assertThat(typeName).isEqualTo("reveal_solution")
assertThat(bundle).hasSize(17)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_SOLUTION_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -1229,14 +1283,64 @@ class EventBundleCreatorTest {
setUpTestApplicationComponentWithLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessSolutionContext())
+ val eventLog = createEventLog(context = createRevealSolutionContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
assertThat(typeName).isEqualTo("reveal_solution")
assertThat(bundle).hasSize(19)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("state_name").isEqualTo(TEST_STATE_NAME)
+ assertThat(bundle).string("ld_learner_id").isEqualTo(TEST_LEARNER_ID)
+ assertThat(bundle).string("ld_install_id").isEqualTo(TEST_INSTALLATION_ID)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingSolutionEvent_studyOff_fillsNonSensitiveFieldsAndRetsName() {
+ setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingSolutionContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_solution")
+ assertThat(bundle).hasSize(17)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("state_name").isEqualTo(TEST_STATE_NAME)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingSolutionEvent_studyOn_fillsAllFieldsAndReturnsName() {
+ setUpTestApplicationComponentWithLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingSolutionContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_solution")
+ assertThat(bundle).hasSize(19)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_SOLUTION_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -2109,16 +2213,23 @@ class EventBundleCreatorTest {
private fun createHintUnlockedContext(hintContext: HintContext = createHintContext()) =
createEventContext(hintContext, EventContextBuilder::setHintUnlockedContext)
- private fun createAccessHintContext(hintContext: HintContext = createHintContext()) =
- createEventContext(hintContext, EventContextBuilder::setAccessHintContext)
+ private fun createRevealHintContext(hintContext: HintContext = createHintContext()) =
+ createEventContext(hintContext, EventContextBuilder::setRevealHintContext)
+
+ private fun createViewExistingHintContext(hintContext: HintContext = createHintContext()) =
+ createEventContext(hintContext, EventContextBuilder::setViewExistingHintContext)
private fun createSolutionUnlockedContext(
explorationContext: ExplorationContext = createExplorationContext()
) = createEventContext(explorationContext, EventContextBuilder::setSolutionUnlockedContext)
- private fun createAccessSolutionContext(
+ private fun createRevealSolutionContext(
+ explorationContext: ExplorationContext = createExplorationContext()
+ ) = createEventContext(explorationContext, EventContextBuilder::setRevealSolutionContext)
+
+ private fun createViewExistingSolutionContext(
explorationContext: ExplorationContext = createExplorationContext()
- ) = createEventContext(explorationContext, EventContextBuilder::setAccessSolutionContext)
+ ) = createEventContext(explorationContext, EventContextBuilder::setViewExistingSolutionContext)
private fun createSubmitAnswerContext(
submitAnswerContext: SubmitAnswerContext = createSubmitAnswerContextDetails()
diff --git a/utility/src/test/java/org/oppia/android/util/logging/KenyaAlphaEventBundleCreatorTest.kt b/utility/src/test/java/org/oppia/android/util/logging/KenyaAlphaEventBundleCreatorTest.kt
index ccc9f65bf3f..4799ce8baca 100644
--- a/utility/src/test/java/org/oppia/android/util/logging/KenyaAlphaEventBundleCreatorTest.kt
+++ b/utility/src/test/java/org/oppia/android/util/logging/KenyaAlphaEventBundleCreatorTest.kt
@@ -19,8 +19,6 @@ import org.junit.runner.RunWith
import org.oppia.android.app.model.EventLog
import org.oppia.android.app.model.EventLog.CardContext
import org.oppia.android.app.model.EventLog.ConceptCardContext
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_HINT_CONTEXT
-import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACCESS_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACTIVITYCONTEXT_NOT_SET
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_BACKGROUND_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.APP_IN_FOREGROUND_CONTEXT
@@ -45,11 +43,15 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STO
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PAUSE_VOICE_OVER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.PLAY_VOICE_OVER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.RESUME_EXPLORATION_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVEAL_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION_UNLOCKED_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_OVER_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SUBMIT_ANSWER_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SWITCH_IN_LESSON_LANGUAGE
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_HINT_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.VIEW_EXISTING_SOLUTION_CONTEXT
import org.oppia.android.app.model.EventLog.ExplorationContext
import org.oppia.android.app.model.EventLog.HintContext
import org.oppia.android.app.model.EventLog.LearnerDetailsContext
@@ -563,18 +565,18 @@ class KenyaAlphaEventBundleCreatorTest {
}
@Test
- fun testFillEventBundle_accessHintContextEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
+ fun testFillEventBundle_revealHintContextEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessHintContext())
+ val eventLog = createEventLog(context = createRevealHintContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
- assertThat(typeName).isEqualTo("access_hint_context")
+ assertThat(typeName).isEqualTo("reveal_hint_context")
assertThat(bundle).hasSize(18)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_HINT_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_HINT_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -588,18 +590,70 @@ class KenyaAlphaEventBundleCreatorTest {
}
@Test
- fun testFillEventBundle_accessHintContextEvent_studyOn_fillsAllFieldsAndReturnsName() {
+ fun testFillEventBundle_revealHintContextEvent_studyOn_fillsAllFieldsAndReturnsName() {
setUpTestApplicationComponentWithLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessHintContext())
+ val eventLog = createEventLog(context = createRevealHintContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
- assertThat(typeName).isEqualTo("access_hint_context")
+ assertThat(typeName).isEqualTo("reveal_hint_context")
assertThat(bundle).hasSize(20)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_HINT_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_HINT_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("ed_topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("ed_story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("ed_exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("ed_session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("ed_exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("ed_state_name").isEqualTo(TEST_STATE_NAME)
+ assertThat(bundle).string("hint_index").isEqualTo(TEST_HINT_INDEX_STR)
+ assertThat(bundle).string("ed_ld_learner_id").isEqualTo(TEST_LEARNER_ID)
+ assertThat(bundle).string("ed_ld_install_id").isEqualTo(TEST_INSTALLATION_ID)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingHintEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
+ setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingHintContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_hint_context")
+ assertThat(bundle).hasSize(18)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_HINT_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("ed_topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("ed_story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("ed_exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("ed_session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("ed_exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("ed_state_name").isEqualTo(TEST_STATE_NAME)
+ assertThat(bundle).string("hint_index").isEqualTo(TEST_HINT_INDEX_STR)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingHintEvent_studyOn_fillsAllFieldsAndReturnsName() {
+ setUpTestApplicationComponentWithLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingHintContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_hint_context")
+ assertThat(bundle).hasSize(20)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_HINT_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -665,18 +719,18 @@ class KenyaAlphaEventBundleCreatorTest {
}
@Test
- fun testFillEventBundle_accessSolutionEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
+ fun testFillEventBundle_revealSolutionEvent_studyOff_fillsOnlyNonSensitiveFieldsAndRetsName() {
setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessSolutionContext())
+ val eventLog = createEventLog(context = createRevealSolutionContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
- assertThat(typeName).isEqualTo("access_solution_context")
+ assertThat(typeName).isEqualTo("reveal_solution_context")
assertThat(bundle).hasSize(17)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_SOLUTION_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -689,18 +743,68 @@ class KenyaAlphaEventBundleCreatorTest {
}
@Test
- fun testFillEventBundle_accessSolutionEvent_studyOn_fillsAllFieldsAndReturnsName() {
+ fun testFillEventBundle_revealSolutionEvent_studyOn_fillsAllFieldsAndReturnsName() {
setUpTestApplicationComponentWithLearnerAnalyticsStudy()
val bundle = Bundle()
- val eventLog = createEventLog(context = createAccessSolutionContext())
+ val eventLog = createEventLog(context = createRevealSolutionContext())
val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
- assertThat(typeName).isEqualTo("access_solution_context")
+ assertThat(typeName).isEqualTo("reveal_solution_context")
assertThat(bundle).hasSize(19)
assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
assertThat(bundle).string("priority").isEqualTo("essential")
- assertThat(bundle).integer("event_type").isEqualTo(ACCESS_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("event_type").isEqualTo(REVEAL_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("state_name").isEqualTo(TEST_STATE_NAME)
+ assertThat(bundle).string("ld_learner_id").isEqualTo(TEST_LEARNER_ID)
+ assertThat(bundle).string("ld_install_id").isEqualTo(TEST_INSTALLATION_ID)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingSolutionEvent_studyOff_fillsNonSensitiveFieldsAndRetsName() {
+ setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingSolutionContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_solution_context")
+ assertThat(bundle).hasSize(17)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_SOLUTION_CONTEXT.number)
+ assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
+ assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
+ assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
+ assertThat(bundle).string("topic_id").isEqualTo(TEST_TOPIC_ID)
+ assertThat(bundle).string("story_id").isEqualTo(TEST_STORY_ID)
+ assertThat(bundle).string("exploration_id").isEqualTo(TEST_EXPLORATION_ID)
+ assertThat(bundle).string("session_id").isEqualTo(TEST_LEARNER_SESSION_ID)
+ assertThat(bundle).string("exploration_version").isEqualTo(TEST_EXPLORATION_VERSION_STR)
+ assertThat(bundle).string("state_name").isEqualTo(TEST_STATE_NAME)
+ }
+
+ @Test
+ fun testFillEventBundle_viewExistingSolutionEvent_studyOn_fillsAllFieldsAndReturnsName() {
+ setUpTestApplicationComponentWithLearnerAnalyticsStudy()
+ val bundle = Bundle()
+
+ val eventLog = createEventLog(context = createViewExistingSolutionContext())
+
+ val typeName = eventBundleCreator.fillEventBundle(eventLog, bundle)
+ assertThat(typeName).isEqualTo("view_existing_solution_context")
+ assertThat(bundle).hasSize(19)
+ assertThat(bundle).longInt("timestamp").isEqualTo(TEST_TIMESTAMP_1)
+ assertThat(bundle).string("priority").isEqualTo("essential")
+ assertThat(bundle).integer("event_type").isEqualTo(VIEW_EXISTING_SOLUTION_CONTEXT.number)
assertThat(bundle).integer("android_sdk").isEqualTo(TEST_ANDROID_SDK_VERSION)
assertThat(bundle).string("app_version_name").isEqualTo(TEST_APP_VERSION_NAME)
assertThat(bundle).integer("app_version_code").isEqualTo(TEST_APP_VERSION_CODE)
@@ -1346,16 +1450,23 @@ class KenyaAlphaEventBundleCreatorTest {
private fun createHintUnlockedContext(hintContext: HintContext = createHintContext()) =
createEventContext(hintContext, EventContextBuilder::setHintUnlockedContext)
- private fun createAccessHintContext(hintContext: HintContext = createHintContext()) =
- createEventContext(hintContext, EventContextBuilder::setAccessHintContext)
+ private fun createRevealHintContext(hintContext: HintContext = createHintContext()) =
+ createEventContext(hintContext, EventContextBuilder::setRevealHintContext)
+
+ private fun createViewExistingHintContext(hintContext: HintContext = createHintContext()) =
+ createEventContext(hintContext, EventContextBuilder::setViewExistingHintContext)
private fun createSolutionUnlockedContext(
explorationContext: ExplorationContext = createExplorationContext()
) = createEventContext(explorationContext, EventContextBuilder::setSolutionUnlockedContext)
- private fun createAccessSolutionContext(
+ private fun createRevealSolutionContext(
+ explorationContext: ExplorationContext = createExplorationContext()
+ ) = createEventContext(explorationContext, EventContextBuilder::setRevealSolutionContext)
+
+ private fun createViewExistingSolutionContext(
explorationContext: ExplorationContext = createExplorationContext()
- ) = createEventContext(explorationContext, EventContextBuilder::setAccessSolutionContext)
+ ) = createEventContext(explorationContext, EventContextBuilder::setViewExistingSolutionContext)
private fun createSubmitAnswerContext(
submitAnswerContext: SubmitAnswerContext = createSubmitAnswerContextDetails()
From fcd75ef5495191457de010b94abe39e44e6a3a0b Mon Sep 17 00:00:00 2001
From: Vishwajith Shettigar
<76042077+Vishwajith-Shettigar@users.noreply.github.com>
Date: Wed, 3 Jul 2024 04:36:30 +0530
Subject: [PATCH 5/6] Fix #5393: Make hasProtoExtra centralized. (#5446)
## Explanation
Fixes #5393
Created `ProtoExtraMatcher` class to make `hasProtoExtra` matcher
centralized.
## Essential Checklist
- [ ] 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: ...".)
- [ ] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [ ] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [ ] 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)).
- [ ] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [ ] 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
---
app/BUILD.bazel | 1 +
.../CompletedStoryListActivityTest.kt | 21 +-----------
.../android/app/faq/FAQListFragmentTest.kt | 21 +-----------
.../android/app/home/HomeActivityTest.kt | 19 +----------
.../app/home/RecentlyPlayedFragmentTest.kt | 20 +----------
.../OngoingTopicListActivityTest.kt | 21 +-----------
.../app/options/OptionsFragmentTest.kt | 20 +----------
.../exploration/ExplorationActivityTest.kt | 19 +----------
.../app/profile/ProfileChooserFragmentTest.kt | 19 -----------
.../ProfileProgressFragmentTest.kt | 20 +----------
.../profile/ProfileEditFragmentTest.kt | 22 +------------
.../android/app/story/StoryActivityTest.kt | 20 +----------
.../NavigationDrawerActivityDebugTest.kt | 18 +---------
.../NavigationDrawerActivityProdTest.kt | 17 +---------
.../android/app/topic/TopicActivityTest.kt | 22 +------------
.../topic/lessons/TopicLessonsFragmentTest.kt | 21 +-----------
.../practice/TopicPracticeFragmentTest.kt | 22 +------------
.../revisioncard/RevisionCardFragmentTest.kt | 19 +----------
.../app/utility/EspressoTestsMatchers.kt | 7 ++++
.../android/app/utility/ProtoExtraMatcher.kt | 33 +++++++++++++++++++
.../app/activity/route/ActivityRouterTest.kt | 21 +-----------
.../android/app/activity/route/BUILD.bazel | 1 +
scripts/assets/test_file_exemptions.textproto | 4 +++
23 files changed, 63 insertions(+), 345 deletions(-)
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/utility/ProtoExtraMatcher.kt
diff --git a/app/BUILD.bazel b/app/BUILD.bazel
index 26ee5509faf..35036cf44fa 100644
--- a/app/BUILD.bazel
+++ b/app/BUILD.bazel
@@ -844,6 +844,7 @@ kt_android_library(
"src/sharedTest/java/org/oppia/android/app/utility/FontSizeMatcher.kt",
"src/sharedTest/java/org/oppia/android/app/utility/OrientationChangeAction.kt",
"src/sharedTest/java/org/oppia/android/app/utility/ProgressMatcher.kt",
+ "src/sharedTest/java/org/oppia/android/app/utility/ProtoExtraMatcher.kt",
"src/sharedTest/java/org/oppia/android/app/utility/TabMatcher.kt",
],
visibility = [
diff --git a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt
index 3f02650ac1d..7a4686d0b01 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt
@@ -14,7 +14,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.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
@@ -22,12 +21,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
import org.hamcrest.CoreMatchers.containsString
-import org.hamcrest.Description
-import org.hamcrest.Matcher
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -53,6 +48,7 @@ import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositi
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.EspressoTestsMatchers.hasProtoExtra
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
@@ -100,7 +96,6 @@ 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.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -507,20 +502,6 @@ class CompletedStoryListActivityTest {
}
}
- 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
- }
- }
- }
-
private fun createCompletedStoryListActivityIntent(internalProfileId: Int): Intent {
return CompletedStoryListActivity.createCompletedStoryListActivityIntent(
ApplicationProvider.getApplicationContext(),
diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt
index 458791c4242..875f395be75 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt
@@ -2,7 +2,6 @@ package org.oppia.android.app.faq
import android.app.Application
import android.content.Context
-import android.content.Intent
import android.content.res.Resources
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
@@ -19,12 +18,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
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.Matchers.allOf
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -51,6 +46,7 @@ import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositi
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
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
@@ -93,7 +89,6 @@ 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.logging.EventLoggingConfigurationModule
@@ -229,20 +224,6 @@ class FAQListFragmentTest {
}.build()
}
- 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
- }
- }
- }
-
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt
index e9ab96f7703..c32d70aa018 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt
@@ -21,7 +21,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.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -31,12 +30,10 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Description
-import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.hamcrest.core.IsNot.not
import org.junit.After
@@ -84,6 +81,7 @@ import org.oppia.android.app.topic.TopicActivity
import org.oppia.android.app.translation.AppLanguageLocaleHandler
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
import org.oppia.android.app.translation.testing.TestActivityRecreator
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.app.utility.FontScaleConfigurationUtil
import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
@@ -141,7 +139,6 @@ 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.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -2034,20 +2031,6 @@ class HomeActivityTest {
dataProviderTestMonitor.waitForNextSuccessfulResult(profileTestHelper.logIntoUser())
}
- 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(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt
index 54dbac65f1a..1120d96bd29 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt
@@ -27,15 +27,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
-import org.hamcrest.Description
-import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.not
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -69,6 +65,7 @@ import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.hasGridI
import org.oppia.android.app.resumelesson.ResumeLessonActivity
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable
import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
@@ -123,7 +120,6 @@ 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.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -1458,20 +1454,6 @@ class RecentlyPlayedFragmentTest {
.commitNow()
}
- 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(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt
index 0f349ace1ef..0e1bdd16da1 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt
@@ -14,19 +14,14 @@ 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.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
import org.hamcrest.CoreMatchers.containsString
-import org.hamcrest.Description
-import org.hamcrest.Matcher
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -52,6 +47,7 @@ import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositi
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.EspressoTestsMatchers.hasProtoExtra
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
@@ -98,7 +94,6 @@ 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.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -452,20 +447,6 @@ class OngoingTopicListActivityTest {
}
}
- 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
- }
- }
- }
-
private fun createOngoingTopicListActivityIntent(internalProfileId: Int): Intent {
return OngoingTopicListActivity.createOngoingTopicListActivityIntent(
ApplicationProvider.getApplicationContext(),
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt
index 30a1a8bfc0c..5cb9b71ef75 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt
@@ -22,12 +22,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
-import com.google.protobuf.MessageLite
import dagger.Component
-import org.hamcrest.Description
-import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -56,6 +52,7 @@ import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionMo
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
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
@@ -103,7 +100,6 @@ 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.logging.EventLoggingConfigurationModule
@@ -604,20 +600,6 @@ class OptionsFragmentTest {
testCoroutineDispatchers.runCurrent()
}
- 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(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt
index e8557225acc..df5dfbf4c28 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt
@@ -26,7 +26,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.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isChecked
@@ -42,7 +41,6 @@ import androidx.test.espresso.util.TreeIterables
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
import org.hamcrest.BaseMatcher
import org.hamcrest.CoreMatchers.allOf
@@ -50,7 +48,6 @@ import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.not
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Ignore
@@ -86,6 +83,7 @@ import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositi
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.testing.ExplorationInjectionActivity
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable
import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
@@ -156,7 +154,6 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.accessibility.FakeAccessibilityService
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.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -2451,20 +2448,6 @@ class ExplorationActivityTest {
}
}
- 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
- }
- }
- }
-
private fun markSpotlightSeen(feature: Spotlight.FeatureCase) {
val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build()
spotlightStateController.markSpotlightViewed(profileId, feature)
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 b24ca39d366..322f5e2855d 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
@@ -23,12 +23,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
-import org.hamcrest.Description
-import org.hamcrest.Matcher
import org.hamcrest.Matchers.not
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -103,7 +99,6 @@ 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.logging.EventLoggingConfigurationModule
@@ -554,20 +549,6 @@ class ProfileChooserFragmentTest {
)
}
- 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
- }
- }
- }
-
private fun verifyTextOnProfileListItemAtPosition(
itemPosition: Int,
targetView: Int,
diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt
index 097e4aafb76..2528d34e73a 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt
@@ -23,7 +23,6 @@ import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
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.intent.matcher.IntentMatchers.hasType
import androidx.test.espresso.matcher.RootMatchers.isDialog
@@ -35,15 +34,12 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.material.card.MaterialCardView
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
import dagger.Module
import dagger.Provides
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
-import org.hamcrest.Description
import org.hamcrest.Matcher
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -74,6 +70,7 @@ import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositi
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.EspressoTestsMatchers.hasProtoExtra
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
@@ -122,7 +119,6 @@ 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.logging.EnableConsoleLog
@@ -872,20 +868,6 @@ class ProfileProgressFragmentTest {
.check(matches(isDisplayed()))
}
- 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
- }
- }
- }
-
@Module
class TestModule {
// TODO(#59): Either isolate these to their own shared test module, or use the real logging
diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt
index cdd5932a59a..37992371629 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt
@@ -2,7 +2,6 @@ package org.oppia.android.app.settings.profile
import android.app.Application
import android.content.Context
-import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
@@ -13,7 +12,6 @@ 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.hasExtra
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isClickable
@@ -25,12 +23,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
-import org.hamcrest.Description
-import org.hamcrest.Matcher
import org.hamcrest.Matchers.not
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -57,6 +51,7 @@ import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.testing.ProfileEditFragmentTestActivity
import org.oppia.android.app.testing.ProfileEditFragmentTestActivity.Companion.createProfileEditFragmentTestActivity
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
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
@@ -105,7 +100,6 @@ 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.data.DataProviders.Companion.toLiveData
-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.logging.EventLoggingConfigurationModule
@@ -479,20 +473,6 @@ class ProfileEditFragmentTest {
assertThat(profile.allowInLessonQuickLanguageSwitching).isTrue()
}
- 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
- }
- }
- }
-
private fun launchFragmentTestActivity(internalProfileId: Int) =
launch(
createProfileEditFragmentTestActivity(context, internalProfileId)
diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt
index f92c76aeee6..3b5364c992f 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt
@@ -18,12 +18,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.Component
-import org.hamcrest.Description
-import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -48,6 +44,7 @@ import org.oppia.android.app.player.exploration.ExplorationActivity
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.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
import org.oppia.android.data.backends.gae.NetworkModule
import org.oppia.android.domain.classify.InteractionsModule
@@ -95,7 +92,6 @@ 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.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -237,20 +233,6 @@ class StoryActivityTest {
)
}
- 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(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt
index ad92595d275..e367b50c81a 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt
@@ -24,7 +24,6 @@ import androidx.test.espresso.contrib.DrawerMatchers.isOpen
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.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.hasTextColor
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
@@ -38,7 +37,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.util.HumanReadables
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.material.navigation.NavigationView
-import com.google.protobuf.MessageLite
import dagger.Component
import org.hamcrest.Description
import org.hamcrest.Matcher
@@ -67,6 +65,7 @@ import org.oppia.android.app.model.ProfileId
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.app.utility.EspressoTestsMatchers.hasProtoExtra
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
@@ -113,7 +112,6 @@ 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.logging.EventLoggingConfigurationModule
@@ -340,20 +338,6 @@ class NavigationDrawerActivityDebugTest {
}
}
- 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
- }
- }
- }
-
private fun ActivityScenario.openNavigationDrawer() {
onView(withContentDescription(R.string.drawer_open_content_description))
.check(matches(isCompletelyDisplayed()))
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt
index c3b03432418..c5a79951ef5 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt
@@ -39,7 +39,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.util.HumanReadables
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.material.navigation.NavigationView
-import com.google.protobuf.MessageLite
import dagger.Component
import org.hamcrest.Description
import org.hamcrest.Matcher
@@ -73,6 +72,7 @@ import org.oppia.android.app.profile.ProfileChooserActivity
import org.oppia.android.app.profileprogress.ProfileProgressActivity
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
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
@@ -121,7 +121,6 @@ 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.logging.EventLoggingConfigurationModule
@@ -993,20 +992,6 @@ class NavigationDrawerActivityProdTest {
}
}
- 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(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt
index fd95954b8d5..289deeb0f4e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt
@@ -2,7 +2,6 @@ package org.oppia.android.app.topic
import android.app.Application
import android.content.Context
-import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
@@ -14,17 +13,12 @@ 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.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-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.Rule
@@ -51,6 +45,7 @@ import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionMo
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.topic.questionplayer.QuestionPlayerActivity
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
import org.oppia.android.data.backends.gae.NetworkModule
import org.oppia.android.domain.classify.InteractionsModule
@@ -98,7 +93,6 @@ 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.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
@@ -210,20 +204,6 @@ class TopicActivityTest {
}
}
- 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
- }
- }
- }
-
private fun launchTopicActivity(
internalProfileId: Int,
topicId: String
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt
index eeda1426fbf..b7cef7f3d89 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt
@@ -16,7 +16,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.matcher.ViewMatchers.hasContentDescription
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isClickable
@@ -27,14 +26,10 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
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.Matchers.allOf
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.not
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -71,6 +66,7 @@ import org.oppia.android.app.story.StoryActivity.Companion.STORY_ACTIVITY_PARAMS
import org.oppia.android.app.topic.TopicActivity
import org.oppia.android.app.topic.TopicTab
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable
import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
@@ -127,7 +123,6 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.accessibility.FakeAccessibilityService
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.logging.EventLoggingConfigurationModule
@@ -1162,20 +1157,6 @@ class TopicLessonsFragmentTest {
).check(matches(withDrawable(R.drawable.ic_check_24dp)))
}
- 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(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt
index 7b0f5057039..13ea4f3bf82 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt
@@ -1,7 +1,6 @@
package org.oppia.android.app.topic.practice
import android.app.Application
-import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
@@ -13,7 +12,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.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
@@ -22,13 +20,9 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
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.Matchers.allOf
import org.hamcrest.Matchers.not
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -59,6 +53,7 @@ import org.oppia.android.app.topic.TopicTab
import org.oppia.android.app.topic.questionplayer.QuestionPlayerActivity
import org.oppia.android.app.topic.questionplayer.QuestionPlayerActivity.Companion.QUESTION_PLAYER_ACTIVITY_PARAMS_KEY
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
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
@@ -104,7 +99,6 @@ 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.logging.EventLoggingConfigurationModule
@@ -442,20 +436,6 @@ class TopicPracticeFragmentTest {
testCoroutineDispatchers.runCurrent()
}
- 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(
diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
index d70c6c13791..b57ef1fb0e3 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt
@@ -2,7 +2,6 @@ package org.oppia.android.app.topic.revisioncard
import android.app.Application
import android.content.Context
-import android.content.Intent
import android.text.Spannable
import android.text.style.ClickableSpan
import android.view.View
@@ -20,7 +19,6 @@ 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.hasExtra
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -28,7 +26,6 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.protobuf.MessageLite
import dagger.Component
import dagger.Module
import dagger.Provides
@@ -67,6 +64,7 @@ import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionMo
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.topic.revisioncard.RevisionCardActivity.Companion.createRevisionCardActivityIntent
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
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
@@ -122,7 +120,6 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.LoadImagesFromAssets
import org.oppia.android.util.caching.LoadLessonProtosFromAssets
-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.logging.EventLoggingConfigurationModule
@@ -736,20 +733,6 @@ class RevisionCardFragmentTest {
}
}
- 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
- }
- }
- }
-
/** See the version in StateFragmentTest for documentation details. */
private fun hasClickableSpanWithText(text: String): Matcher {
return object : TypeSafeMatcher(TextView::class.java) {
diff --git a/app/src/sharedTest/java/org/oppia/android/app/utility/EspressoTestsMatchers.kt b/app/src/sharedTest/java/org/oppia/android/app/utility/EspressoTestsMatchers.kt
index 9ba196eb585..1f57e636ff9 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/utility/EspressoTestsMatchers.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/utility/EspressoTestsMatchers.kt
@@ -1,7 +1,10 @@
package org.oppia.android.app.utility
+import android.content.Intent
import android.view.View
+import com.google.protobuf.MessageLite
import org.hamcrest.Matcher
+import org.oppia.android.app.utility.ProtoExtraMatcher.Companion.hasProtoExtraCheck
import org.oppia.android.app.utility.TabMatcher.Companion.matchCurrentTabTitleCheck
// https://medium.com/@dbottillo/android-ui-test-espresso-matcher-for-imageview-1a28c832626f#.4snjg8frw
@@ -20,4 +23,8 @@ object EspressoTestsMatchers {
fun noDrawable(): Matcher {
return DrawableMatcher(DrawableMatcher.NONE)
}
+
+ fun hasProtoExtra(keyName: String, expectedProto: T): Matcher {
+ return hasProtoExtraCheck(keyName, expectedProto)
+ }
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/utility/ProtoExtraMatcher.kt b/app/src/sharedTest/java/org/oppia/android/app/utility/ProtoExtraMatcher.kt
new file mode 100644
index 00000000000..1b8a38154b6
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/utility/ProtoExtraMatcher.kt
@@ -0,0 +1,33 @@
+package org.oppia.android.app.utility
+
+import android.content.Intent
+import com.google.protobuf.MessageLite
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.TypeSafeMatcher
+import org.oppia.android.util.extensions.getProtoExtra
+
+/** A custom matcher to check if an Intent has a specific proto extra. */
+class ProtoExtraMatcher {
+ companion object {
+ /**
+ * Returns a matcher that verifies if an Intent contains a specific proto extra.
+ *
+ * @param keyName The key name of the proto extra in the Intent.
+ * @param expectedProto The expected proto message to be matched.
+ */
+ fun hasProtoExtraCheck(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
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/test/java/org/oppia/android/app/activity/route/ActivityRouterTest.kt b/app/src/test/java/org/oppia/android/app/activity/route/ActivityRouterTest.kt
index 018f6c22194..e493e5a6120 100644
--- a/app/src/test/java/org/oppia/android/app/activity/route/ActivityRouterTest.kt
+++ b/app/src/test/java/org/oppia/android/app/activity/route/ActivityRouterTest.kt
@@ -1,7 +1,6 @@
package org.oppia.android.app.activity.route
import android.app.Application
-import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.intent.Intents
@@ -10,13 +9,9 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-import com.google.protobuf.MessageLite
import dagger.BindsInstance
import dagger.Component
import org.hamcrest.CoreMatchers.allOf
-import org.hamcrest.Description
-import org.hamcrest.Matcher
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -42,6 +37,7 @@ import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionMo
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.testing.activity.TestActivity
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra
import org.oppia.android.data.backends.gae.NetworkConfigProdModule
import org.oppia.android.data.backends.gae.NetworkModule
import org.oppia.android.domain.classify.InteractionsModule
@@ -82,7 +78,6 @@ 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.logging.EventLoggingConfigurationModule
@@ -166,20 +161,6 @@ class ActivityRouterTest {
}
}
- 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
- }
- }
- }
-
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
diff --git a/app/src/test/java/org/oppia/android/app/activity/route/BUILD.bazel b/app/src/test/java/org/oppia/android/app/activity/route/BUILD.bazel
index b10f9b90038..250d3e8bf15 100644
--- a/app/src/test/java/org/oppia/android/app/activity/route/BUILD.bazel
+++ b/app/src/test/java/org/oppia/android/app/activity/route/BUILD.bazel
@@ -13,6 +13,7 @@ oppia_android_test(
deps = [
"//:dagger",
"//app",
+ "//app:test_deps",
"//app/src/main/java/org/oppia/android/app/activity:activity_intent_factories_shim",
"//app/src/main/java/org/oppia/android/app/application:application_component",
"//app/src/main/java/org/oppia/android/app/application:application_injector",
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index d20ba9e9482..b240ce632ee 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -2521,6 +2521,10 @@ test_file_exemption {
exempted_file_path: "app/src/sharedTest/java/org/oppia/android/app/utility/ProgressMatcher.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/sharedTest/java/org/oppia/android/app/utility/ProtoExtraMatcher.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/sharedTest/java/org/oppia/android/app/utility/TabMatcher.kt"
test_file_not_required: true
From 1c4b17a10fc42dd8efb14d7e7f4841ddff598607 Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Wed, 3 Jul 2024 03:07:57 +0300
Subject: [PATCH 6/6] Fix Part of #4938: Introduce Onboarding Audio language
screen (#5386)
## Explanation
Fix Part of https://github.com/oppia/oppia-android/issues/4938: New
screen to allow a user to select an audio language during the onboarding
process.
This PR only adds the layout and navigation functions without
implementing the language selection functionality.
||Portrait|Landscape|
|--|--|--|
|Mobile|![Screenshot_1719963278](https://github.com/oppia/oppia-android/assets/59600948/8f6e9f7b-0362-4172-9a46-036661b71598)|![Screenshot_1719963292](https://github.com/oppia/oppia-android/assets/59600948/19c1124c-1c63-4b71-b678-597db2f005d8)|
||![Screenshot_1719963273](https://github.com/oppia/oppia-android/assets/59600948/324726d9-8fdb-466d-82d0-71b7ddc3581a)|![Screenshot_1719963301](https://github.com/oppia/oppia-android/assets/59600948/8d3c1b27-217e-4e1d-b633-20368a057569)|
|Tablet|![Screenshot_1719963641](https://github.com/oppia/oppia-android/assets/59600948/1b53195b-2c17-4a15-8c95-6090fa77470e)|![Screenshot_1719963651](https://github.com/oppia/oppia-android/assets/59600948/49300ebf-bfae-4a42-922d-64b591bbf138)|
||![Screenshot_1719963670](https://github.com/oppia/oppia-android/assets/59600948/cf4e37d3-56f0-47c4-9f0d-b01d75cceb8d)|![Screenshot_1719963660](https://github.com/oppia/oppia-android/assets/59600948/c4b8f6f7-a41e-44a0-af7f-1c8bfca9fa4f)|
## 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)).
---
.../AudioLanguageFragmentPresenter.kt | 73 ++++++++
.../app/onboarding/IntroFragmentPresenter.kt | 11 ++
.../app/options/AudioLanguageFragment.kt | 29 ++-
...kt => AudioLanguageFragmentPresenterV1.kt} | 2 +-
.../AudioLanguageSelectionViewModel.kt | 13 ++
.../audio_language_selection_fragment.xml | 96 ++++++++++
...arding_app_language_selection_fragment.xml | 10 +-
.../audio_language_selection_fragment.xml | 112 ++++++++++++
...arding_app_language_selection_fragment.xml | 10 +-
.../audio_language_selection_fragment.xml | 121 +++++++++++++
...arding_app_language_selection_fragment.xml | 10 +-
.../audio_language_selection_fragment.xml | 126 +++++++++++++
...arding_app_language_selection_fragment.xml | 10 +-
.../onboarding_language_dropdown_item.xml | 17 ++
app/src/main/res/values/dimens.xml | 1 +
app/src/main/res/values/strings.xml | 5 +
app/src/main/res/values/styles.xml | 28 +++
.../app/options/AudioLanguageFragmentTest.kt | 165 +++++++++++++++++-
scripts/assets/test_file_exemptions.textproto | 25 ++-
19 files changed, 813 insertions(+), 51 deletions(-)
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
rename app/src/main/java/org/oppia/android/app/options/{AudioLanguageFragmentPresenter.kt => AudioLanguageFragmentPresenterV1.kt} (97%)
create mode 100644 app/src/main/res/layout-land/audio_language_selection_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml
create mode 100644 app/src/main/res/layout/audio_language_selection_fragment.xml
create mode 100644 app/src/main/res/layout/onboarding_language_dropdown_item.xml
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
new file mode 100644
index 00000000000..3a238d4b010
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
@@ -0,0 +1,73 @@
+package org.oppia.android.app.onboarding
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.ArrayAdapter
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import com.google.android.material.appbar.AppBarLayout
+import org.oppia.android.R
+import org.oppia.android.app.options.AudioLanguageSelectionViewModel
+import org.oppia.android.app.translation.AppLanguageResourceHandler
+import org.oppia.android.databinding.AudioLanguageSelectionFragmentBinding
+import javax.inject.Inject
+
+/** The presenter for [AudioLanguageFragment]. */
+class AudioLanguageFragmentPresenter @Inject constructor(
+ private val fragment: Fragment,
+ private val activity: AppCompatActivity,
+ private val appLanguageResourceHandler: AppLanguageResourceHandler,
+ private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel
+) {
+ private lateinit var binding: AudioLanguageSelectionFragmentBinding
+
+ /**
+ * Returns a newly inflated view to render the fragment with an evaluated audio language as the
+ * initial selected language, based on current locale.
+ */
+ fun handleCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?
+ ): View {
+
+ // Hide toolbar as it's not needed in this layout. The toolbar is created by a shared activity
+ // and is required in OptionsFragment.
+ activity.findViewById(R.id.reading_list_app_bar_layout).visibility = View.GONE
+
+ binding = AudioLanguageSelectionFragmentBinding.inflate(
+ inflater,
+ container,
+ /* attachToRoot= */ false
+ )
+ binding.lifecycleOwner = fragment
+
+ binding.audioLanguageText.text = appLanguageResourceHandler.getStringInLocaleWithWrapping(
+ R.string.audio_language_fragment_text,
+ appLanguageResourceHandler.getStringInLocale(R.string.app_name)
+ )
+
+ binding.onboardingNavigationBack.setOnClickListener {
+ activity.finish()
+ }
+
+ val adapter = ArrayAdapter(
+ fragment.requireContext(),
+ R.layout.onboarding_language_dropdown_item,
+ R.id.onboarding_language_text_view,
+ audioLanguageSelectionViewModel.availableAudioLanguages
+ )
+
+ binding.audioLanguageDropdownList.apply {
+ setAdapter(adapter)
+ setText(
+ audioLanguageSelectionViewModel.defaultLanguageSelection,
+ false
+ )
+ setRawInputType(EditorInfo.TYPE_NULL)
+ }
+
+ return binding.root
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt
index ac0b9d04401..50fa51300c7 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt
@@ -6,6 +6,8 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import org.oppia.android.R
+import org.oppia.android.app.model.AudioLanguage
+import org.oppia.android.app.options.AudioLanguageActivity
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.databinding.LearnerIntroFragmentBinding
import javax.inject.Inject
@@ -29,6 +31,7 @@ class IntroFragmentPresenter @Inject constructor(
container,
/* attachToRoot= */ false
)
+
binding.lifecycleOwner = fragment
setLearnerName(profileNickname)
@@ -43,6 +46,14 @@ class IntroFragmentPresenter @Inject constructor(
appLanguageResourceHandler.getStringInLocale(R.string.app_name)
)
+ binding.onboardingNavigationContinue.setOnClickListener {
+ val intent = AudioLanguageActivity.createAudioLanguageActivityIntent(
+ fragment.requireContext(),
+ AudioLanguage.ENGLISH_AUDIO_LANGUAGE
+ )
+ fragment.startActivity(intent)
+ }
+
return binding.root
}
diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt
index fd98e6259cd..71ea48ca09e 100644
--- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt
@@ -10,14 +10,23 @@ import org.oppia.android.app.fragment.InjectableFragment
import org.oppia.android.app.model.AudioLanguage
import org.oppia.android.app.model.AudioLanguageFragmentArguments
import org.oppia.android.app.model.AudioLanguageFragmentStateBundle
+import org.oppia.android.app.onboarding.AudioLanguageFragmentPresenter
import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.extensions.putProto
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
+import org.oppia.android.util.platformparameter.PlatformParameterValue
import javax.inject.Inject
/** The fragment to change the default audio language of the app. */
class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonListener {
+ @Inject lateinit var audioLanguageFragmentPresenterV1: AudioLanguageFragmentPresenterV1
+
@Inject lateinit var audioLanguageFragmentPresenter: AudioLanguageFragmentPresenter
+ @Inject
+ @field:EnableOnboardingFlowV2
+ lateinit var enableOnboardingFlowV2: PlatformParameterValue
+
override fun onAttach(context: Context) {
super.onAttach(context)
(fragmentComponent as FragmentComponentImpl).inject(this)
@@ -33,19 +42,27 @@ class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonList
savedInstanceState?.retrieveLanguageFromSavedState()
?: arguments?.retrieveLanguageFromArguments()
) { "Expected arguments to be passed to AudioLanguageFragment" }
- return audioLanguageFragmentPresenter.handleOnCreateView(inflater, container, audioLanguage)
+ return if (enableOnboardingFlowV2.value) {
+ audioLanguageFragmentPresenter.handleCreateView(inflater, container)
+ } else {
+ audioLanguageFragmentPresenterV1.handleOnCreateView(inflater, container, audioLanguage)
+ }
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
- val state = AudioLanguageFragmentStateBundle.newBuilder().apply {
- audioLanguage = audioLanguageFragmentPresenter.getLanguageSelected()
- }.build()
- outState.putProto(FRAGMENT_SAVED_STATE_KEY, state)
+ if (!enableOnboardingFlowV2.value) {
+ val state = AudioLanguageFragmentStateBundle.newBuilder().apply {
+ audioLanguage = audioLanguageFragmentPresenterV1.getLanguageSelected()
+ }.build()
+ outState.putProto(FRAGMENT_SAVED_STATE_KEY, state)
+ }
}
override fun onLanguageSelected(audioLanguage: AudioLanguage) {
- audioLanguageFragmentPresenter.onLanguageSelected(audioLanguage)
+ if (!enableOnboardingFlowV2.value) {
+ audioLanguageFragmentPresenterV1.onLanguageSelected(audioLanguage)
+ }
}
companion object {
diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenterV1.kt
similarity index 97%
rename from app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt
rename to app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenterV1.kt
index 5195adcebe1..72774fec6ba 100644
--- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenterV1.kt
@@ -11,7 +11,7 @@ import org.oppia.android.databinding.AudioLanguageItemBinding
import javax.inject.Inject
/** The presenter for [AudioLanguageFragment]. */
-class AudioLanguageFragmentPresenter @Inject constructor(
+class AudioLanguageFragmentPresenterV1 @Inject constructor(
private val fragment: Fragment,
private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel,
private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory
diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt
index c9e0d998e1b..5e25aaabf5d 100644
--- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt
+++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageSelectionViewModel.kt
@@ -31,6 +31,19 @@ class AudioLanguageSelectionViewModel @Inject constructor(
)
}
+ // TODO(#4938): Update the pre-selection logic.
+ /** The pre-selected [AudioLanguage] to be shown in the language selection dropdown. */
+ val defaultLanguageSelection = getLanguageDisplayName(AudioLanguage.ENGLISH_AUDIO_LANGUAGE)
+
+ /** The list of [AudioLanguage]s supported by the app. */
+ val availableAudioLanguages: List by lazy {
+ AudioLanguage.values().filter { it !in IGNORED_AUDIO_LANGUAGES }.map(::getLanguageDisplayName)
+ }
+
+ private fun getLanguageDisplayName(audioLanguage: AudioLanguage): String {
+ return appLanguageResourceHandler.computeLocalizedDisplayName(audioLanguage)
+ }
+
private companion object {
private val IGNORED_AUDIO_LANGUAGES =
listOf(
diff --git a/app/src/main/res/layout-land/audio_language_selection_fragment.xml b/app/src/main/res/layout-land/audio_language_selection_fragment.xml
new file mode 100644
index 00000000000..ed683db064e
--- /dev/null
+++ b/app/src/main/res/layout-land/audio_language_selection_fragment.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml
index 45941fad82e..06652937c5a 100644
--- a/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout-land/onboarding_app_language_selection_fragment.xml
@@ -91,15 +91,7 @@
card_view:cardUseCompatPadding="false">
diff --git a/app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml
new file mode 100644
index 00000000000..157f25a8040
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/audio_language_selection_fragment.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml
index 3e39ed471b3..a319e663457 100644
--- a/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-land/onboarding_app_language_selection_fragment.xml
@@ -100,15 +100,7 @@
card_view:cardUseCompatPadding="false">
diff --git a/app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml
new file mode 100644
index 00000000000..08adbf3496e
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-port/audio_language_selection_fragment.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml
index d631c75b742..9425ffc352d 100644
--- a/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout-sw600dp-port/onboarding_app_language_selection_fragment.xml
@@ -99,15 +99,7 @@
card_view:cardUseCompatPadding="false">
diff --git a/app/src/main/res/layout/audio_language_selection_fragment.xml b/app/src/main/res/layout/audio_language_selection_fragment.xml
new file mode 100644
index 00000000000..77eb7eca1de
--- /dev/null
+++ b/app/src/main/res/layout/audio_language_selection_fragment.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml b/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml
index cdb7864b405..2c711918350 100644
--- a/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml
+++ b/app/src/main/res/layout/onboarding_app_language_selection_fragment.xml
@@ -95,15 +95,7 @@
card_view:cardUseCompatPadding="false">
diff --git a/app/src/main/res/layout/onboarding_language_dropdown_item.xml b/app/src/main/res/layout/onboarding_language_dropdown_item.xml
new file mode 100644
index 00000000000..2e5dfb53e28
--- /dev/null
+++ b/app/src/main/res/layout/onboarding_language_dropdown_item.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 1f521f91bfe..0b06f5569ba 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -794,6 +794,7 @@
28dp
20dp
3dp
+ 24sp
144dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e935ea76583..1395b1d24a3 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -677,4 +677,9 @@
Mama and baby otter.
Back
Continue
+
+
+ In %s, you can listen to lessons!
+ Select the audio language to listen to lessons
+ STEP 5 OF 5
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 2231353378b..61eaea05390 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -788,4 +788,32 @@
- @drawable/ic_green_check
- @color/component_color_onboarding_learner_intro_check_color
+
+
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt
index 5fdbbf9c29b..b25607c9158 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt
@@ -9,12 +9,17 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import dagger.Component
-import org.junit.Before
+import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -66,7 +71,6 @@ 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
@@ -74,6 +78,7 @@ 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.platformparameter.TestPlatformParameterModule
import org.oppia.android.testing.profile.ProfileTestHelper
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
@@ -119,14 +124,16 @@ class AudioLanguageFragmentTest {
@Inject lateinit var profileTestHelper: ProfileTestHelper
@Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
- @Before
- fun setUp() {
- setUpTestApplicationComponent()
- profileTestHelper.initializeProfiles()
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ TestPlatformParameterModule.reset()
+ Intents.release()
}
@Test
fun testOpenFragment_withEnglish_selectedLanguageIsEnglish() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
verifyEnglishIsSelected()
}
@@ -134,6 +141,7 @@ class AudioLanguageFragmentTest {
@Test
fun testOpenFragment_withPortuguese_selectedLanguageIsPortuguese() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(BRAZILIAN_PORTUGUESE_LANGUAGE).use {
verifyPortugueseIsSelected()
}
@@ -141,6 +149,7 @@ class AudioLanguageFragmentTest {
@Test
fun testOpenFragment_withNigerianPidgin_selectedLanguageIsNaija() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(NIGERIAN_PIDGIN_LANGUAGE).use {
verifyNigerianPidginIsSelected()
}
@@ -148,6 +157,7 @@ class AudioLanguageFragmentTest {
@Test
fun testAudioLanguage_configChange_selectedLanguageIsEnglish() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
rotateToLandscape()
@@ -158,6 +168,7 @@ class AudioLanguageFragmentTest {
@Test
@Config(qualifiers = "sw600dp")
fun testAudioLanguage_tabletConfig_selectedLanguageIsEnglish() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
testCoroutineDispatchers.runCurrent()
@@ -167,6 +178,7 @@ class AudioLanguageFragmentTest {
@Test
fun testAudioLanguage_changeLanguageToPortuguese_selectedLanguageIsPortuguese() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
selectPortuguese()
@@ -176,6 +188,7 @@ class AudioLanguageFragmentTest {
@Test
fun testAudioLanguage_changeLanguageToPortuguese_configChange_selectedLanguageIsPortuguese() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
selectPortuguese()
@@ -188,6 +201,7 @@ class AudioLanguageFragmentTest {
@Test
@Config(qualifiers = "sw600dp")
fun testAudioLanguage_configChange_changeLanguageToPortuguese_selectedLanguageIsPortuguese() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
rotateToLandscape()
@@ -199,6 +213,7 @@ class AudioLanguageFragmentTest {
@Test
fun testAudioLanguage_selectPortuguese_thenEnglish_selectedLanguageIsPortuguese() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = false)
launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
selectPortuguese()
@@ -208,6 +223,134 @@ class AudioLanguageFragmentTest {
}
}
+ @Test
+ fun testAudioLanguage_onboardingV2Enabled_allViewsAreDisplayed() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
+ onView(withId(R.id.audio_language_text)).check(
+ matches(withText("In Oppia, you can listen to lessons!"))
+ )
+ onView(withId(R.id.audio_language_subtitle)).check(
+ matches(withText(context.getString(R.string.audio_language_fragment_subtitle)))
+ )
+ onView(withId(R.id.audio_language_dropdown_list)).check(
+ matches(withText(context.getString(R.string.english_localized_language_name)))
+ )
+ onView(withId(R.id.onboarding_navigation_back)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ onView(withId(R.id.onboarding_navigation_continue)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ }
+ }
+
+ @Test
+ fun testAudioLanguage_onboardingV2Enabled_configChange_allViewsAreDisplayed() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launchActivityWithLanguage(ENGLISH_AUDIO_LANGUAGE).use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.audio_language_text)).check(
+ matches(withText("In Oppia, you can listen to lessons!"))
+ )
+ onView(withId(R.id.audio_language_subtitle)).check(
+ matches(withText(context.getString(R.string.audio_language_fragment_subtitle)))
+ )
+ onView(withId(R.id.audio_language_dropdown_list)).check(
+ matches(withText(context.getString(R.string.english_localized_language_name)))
+ )
+ onView(withId(R.id.onboarding_navigation_back)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ onView(withId(R.id.onboarding_navigation_continue)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_backButtonPressed_currentScreenIsDestroyed() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launch(
+ createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
+ ).use { scenario ->
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_backButtonPressed_currentScreenIsDestroyed() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launch(
+ createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
+ ).use { scenario ->
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_continueButtonClicked_launchesHomeScreen() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launch(
+ createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
+ ).use {
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // Do nothing for now, but will fail once navigation is implemented
+ onView(withId(R.id.audio_language_text)).check(
+ matches(withText("In Oppia, you can listen to lessons!"))
+ )
+ onView(withId(R.id.audio_language_subtitle)).check(
+ matches(withText(context.getString(R.string.audio_language_fragment_subtitle)))
+ )
+ onView(withId(R.id.onboarding_navigation_back)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ onView(withId(R.id.onboarding_navigation_continue)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_continueButtonClicked_launchesHomeScreen() {
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launch(
+ createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
+ ).use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // Do nothing for now, but will fail once navigation is implemented
+ onView(withId(R.id.audio_language_text)).check(
+ matches(withText("In Oppia, you can listen to lessons!"))
+ )
+ onView(withId(R.id.audio_language_subtitle)).check(
+ matches(withText(context.getString(R.string.audio_language_fragment_subtitle)))
+ )
+ onView(withId(R.id.onboarding_navigation_back)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ onView(withId(R.id.onboarding_navigation_continue)).check(
+ matches(withEffectiveVisibility(Visibility.VISIBLE))
+ )
+ }
+ }
+
private fun launchActivityWithLanguage(
audioLanguage: AudioLanguage
): ActivityScenario {
@@ -272,6 +415,14 @@ class AudioLanguageFragmentTest {
).check(matches(withText(expectedLanguageName)))
}
+ private fun initializeTestApplicationComponent(enableOnboardingFlowV2: Boolean) {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(enableOnboardingFlowV2)
+ Intents.init()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ profileTestHelper.initializeProfiles()
+ }
+
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
@@ -280,7 +431,7 @@ class AudioLanguageFragmentTest {
@Singleton
@Component(
modules = [
- TestDispatcherModule::class, PlatformParameterModule::class, ApplicationModule::class,
+ TestDispatcherModule::class, TestPlatformParameterModule::class, ApplicationModule::class,
RobolectricModule::class, LoggerModule::class, ContinueModule::class,
FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index b240ce632ee..d1ff255fc64 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -1101,6 +1101,29 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt"
+ test_file_not_required: true
+}test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt"
test_file_not_required: true
@@ -1146,7 +1169,7 @@ test_file_exemption {
test_file_not_required: true
}
test_file_exemption {
- exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenter.kt"
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/options/AudioLanguageFragmentPresenterV1.kt"
test_file_not_required: true
}
test_file_exemption {