From 2eeec89b25d8096072f1c5883c5348ea3f77de5a Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Fri, 29 Nov 2024 13:58:31 +0300
Subject: [PATCH] Fix Part of #4938: Profile Configuration and Migration
(#5387)
## Explanation
Fix Part of #4938: Configure profile new creation and migration of
existing profiles.
## When Onboarding V2 is Enabled:
### Sole Learner Onboarding
Once the learner has created a profile, and have landed on the
introduction screen, the profile is marked as
`started_profile_oboarding`, and a corresponding event log is added.
This configuration facilitates the user resuming onboarding, if they
didn't complete it the first time round.
After selecting the audio language, the profile is logged in, and routed
to the home screen.
On the home screen, the profile is updated to indicate
`completed_profile_oboarding` and 3 events are expected for the first
time login flow, in order:
1. Open home event
2. Profile Onboarding Completed event
3. App Onboarding Completed event
The App Onboarding Completed event is only logged for the first profile
on the device, while the Profile Onboarding Completed event is logged
for every profile on first login.
### Supervisor Profile Onboarding
From the Profile Type screen, the supervisor flow launches the profile
chooser screen, showing the Admin profile, which on click launches the
homescreen.
The same 3 events as above are expected for the first time login flow.
### Login Routing
Returning users should be routed to an appropriate landing page as
follows:
A sole learner profile will be routed directly to their home screen
Returning admin profiles and non-solo learner profiles will always be
routed to the profile selection screen
A sole learner who started, but did not finish onboarding would be
routed to the Introduction Screen.
Returning sole and none-sole learners created when the feature flag was
disabled, would be directed to the onboarding screen. Profile migration
happens in place when profiles are fetched by the controller.
### Profile Creation and Migration
For migration purposes, the `getProfile()` function has been updated to
compute the `profileType` field when the feature flag is enabled.
Existing Admin profiles will be migrated to have the SUPERVISOR type,
existing Admin profiles with no pins set will be migrated to have the
SOLE_LEARNER type while the remaining accounts will be of the
ADDITIONAL_LEARNER type. New profiles created will also contain the
respective enum type based on the intended categorization.
Flag off then on:
[device-2024-06-21-073957.webm](https://github.com/oppia/oppia-android/assets/59600948/7584ce32-305b-4743-878e-64c94dc52e66)
Flag on then off:
[device-2024-06-21-074114.webm](https://github.com/oppia/oppia-android/assets/59600948/87860084-c75c-4b6c-a37f-c2459a3e7794)
### Misc
- There are general Kdoc formatting fixes all over the place.
- `ProfileTestHelper` Has been modified to create a sole, pinless admin
profile.
- There are other minor changes to fix tests that have been impacted by
changes in this PR.
- The changes in `DeprecationController` are purely visual to make the
code more readable.
- `ClassroomListFragmentTest` required refactor to support toggling
multiple feature flags.
### Profile Onboarding Events
|Onborading Flow v1| Onboarding Flow v2|
|---|---|
|||
## 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)).
---------
Co-authored-by: Ben Henning
---
.../app/classroom/ClassroomListActivity.kt | 47 ++-
.../ClassroomListFragmentPresenter.kt | 100 ++++-
.../app/drawer/ExitProfileDialogFragment.kt | 16 +-
.../android/app/home/ExitProfileListener.kt | 13 +
.../oppia/android/app/home/HomeActivity.kt | 47 ++-
.../android/app/home/HomeFragmentPresenter.kt | 64 +++-
.../AudioLanguageFragmentPresenter.kt | 37 +-
.../CreateProfileActivityPresenter.kt | 6 +-
.../app/onboarding/IntroFragmentPresenter.kt | 6 +-
.../onboarding/OnboardingFragmentPresenter.kt | 3 +-
.../OnboardingProfileTypeFragmentPresenter.kt | 23 +-
.../app/profile/ProfileChooserActivity.kt | 14 +-
.../ProfileChooserActivityPresenter.kt | 41 ++-
.../ProfileChooserFragmentPresenter.kt | 83 +++--
.../app/splash/SplashActivityPresenter.kt | 152 +++++++-
.../app/testing/HomeFragmentTestActivity.kt | 6 +-
.../testing/NavigationDrawerTestActivity.kt | 7 +-
.../classroom/ClassroomListFragmentTest.kt | 283 +++++++++++++-
.../android/app/home/HomeActivityTest.kt | 161 +++++++-
.../app/onboarding/IntroFragmentTest.kt | 21 ++
.../OnboardingProfileTypeFragmentTest.kt | 35 +-
.../app/options/AudioLanguageFragmentTest.kt | 39 ++
.../app/options/OptionsFragmentTest.kt | 8 -
.../app/profile/ProfileChooserFragmentTest.kt | 83 ++++-
.../org/oppia/android/app/splash/BUILD.bazel | 1 +
.../android/app/splash/SplashActivityTest.kt | 126 ++++++-
.../android/app/home/HomeActivityLocalTest.kt | 143 +++++++-
.../onboarding/AppStartupStateController.kt | 6 +-
.../onboarding/DeprecationController.kt | 26 +-
.../android/domain/oppialogger/OppiaLogger.kt | 27 +-
.../analytics/AnalyticsController.kt | 24 +-
.../profile/ProfileManagementController.kt | 118 +++++-
.../domain/audio/AudioPlayerControllerTest.kt | 7 +
.../ExplorationProgressControllerTest.kt | 7 +
.../domain/oppialogger/OppiaLoggerTest.kt | 21 ++
.../analytics/AnalyticsControllerTest.kt | 26 ++
.../ProfileManagementControllerTest.kt | 345 +++++++++++++++++-
model/src/main/proto/arguments.proto | 15 +
model/src/main/proto/oppia_logger.proto | 31 ++
model/src/main/proto/profile.proto | 28 ++
scripts/assets/test_file_exemptions.textproto | 4 +
.../testing/logging/EventLogSubject.kt | 84 +++++
.../testing/profile/ProfileTestHelper.kt | 26 ++
.../testing/profile/ProfileTestHelperTest.kt | 42 +++
.../util/logging/EventBundleCreator.kt | 18 +
.../EventTypeToHumanReadableNameConverter.kt | 2 +
46 files changed, 2212 insertions(+), 210 deletions(-)
create mode 100644 app/src/main/java/org/oppia/android/app/home/ExitProfileListener.kt
diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt
index c8f075f10bf..a158e5adc4a 100644
--- a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListActivity.kt
@@ -9,6 +9,7 @@ import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
import org.oppia.android.app.activity.route.ActivityRouter
import org.oppia.android.app.drawer.ExitProfileDialogFragment
import org.oppia.android.app.drawer.TAG_SWITCH_PROFILE_DIALOG
+import org.oppia.android.app.home.ExitProfileListener
import org.oppia.android.app.home.RouteToRecentlyPlayedListener
import org.oppia.android.app.home.RouteToTopicListener
import org.oppia.android.app.home.RouteToTopicPlayStoryListener
@@ -16,6 +17,7 @@ import org.oppia.android.app.model.DestinationScreen
import org.oppia.android.app.model.ExitProfileDialogArguments
import org.oppia.android.app.model.HighlightItem
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.RecentlyPlayedActivityParams
import org.oppia.android.app.model.RecentlyPlayedActivityTitle
import org.oppia.android.app.model.ScreenName.CLASSROOM_LIST_ACTIVITY
@@ -23,6 +25,8 @@ import org.oppia.android.app.topic.TopicActivity.Companion.createTopicActivityIn
import org.oppia.android.app.topic.TopicActivity.Companion.createTopicPlayStoryActivityIntent
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
+import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
@@ -32,7 +36,8 @@ class ClassroomListActivity :
InjectableAutoLocalizedAppCompatActivity(),
RouteToTopicListener,
RouteToTopicPlayStoryListener,
- RouteToRecentlyPlayedListener {
+ RouteToRecentlyPlayedListener,
+ ExitProfileListener {
@Inject
lateinit var classroomListActivityPresenter: ClassroomListActivityPresenter
@@ -44,6 +49,10 @@ class ClassroomListActivity :
private var internalProfileId: Int = -1
+ @Inject
+ @field:EnableOnboardingFlowV2
+ lateinit var enableOnboardingFlowV2: PlatformParameterValue
+
companion object {
/** Returns a new [Intent] to route to [ClassroomListActivity] for a specified [profileId]. */
fun createClassroomListActivity(context: Context, profileId: ProfileId?): Intent {
@@ -68,22 +77,6 @@ class ClassroomListActivity :
classroomListActivityPresenter.handleOnRestart()
}
- override fun onBackPressed() {
- val previousFragment =
- supportFragmentManager.findFragmentByTag(TAG_SWITCH_PROFILE_DIALOG)
- if (previousFragment != null) {
- supportFragmentManager.beginTransaction().remove(previousFragment).commitNow()
- }
- val exitProfileDialogArguments =
- ExitProfileDialogArguments
- .newBuilder()
- .setHighlightItem(HighlightItem.NONE)
- .build()
- val dialogFragment = ExitProfileDialogFragment
- .newInstance(exitProfileDialogArguments = exitProfileDialogArguments)
- dialogFragment.showNow(supportFragmentManager, TAG_SWITCH_PROFILE_DIALOG)
- }
-
override fun routeToRecentlyPlayed(recentlyPlayedActivityTitle: RecentlyPlayedActivityTitle) {
val recentlyPlayedActivityParams =
RecentlyPlayedActivityParams
@@ -121,4 +114,24 @@ class ClassroomListActivity :
)
)
}
+
+ override fun exitProfile(profileType: ProfileType) {
+ val previousFragment =
+ supportFragmentManager.findFragmentByTag(TAG_SWITCH_PROFILE_DIALOG)
+ if (previousFragment != null) {
+ supportFragmentManager.beginTransaction().remove(previousFragment).commitNow()
+ }
+ val exitProfileDialogArguments =
+ ExitProfileDialogArguments
+ .newBuilder().apply {
+ if (enableOnboardingFlowV2.value) {
+ this.profileType = profileType
+ }
+ this.highlightItem = HighlightItem.NONE
+ }
+ .build()
+ val dialogFragment = ExitProfileDialogFragment
+ .newInstance(exitProfileDialogArguments = exitProfileDialogArguments)
+ dialogFragment.showNow(supportFragmentManager, TAG_SWITCH_PROFILE_DIALOG)
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt
index 6ced14b9960..b0ab600338e 100644
--- a/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/classroom/ClassroomListFragmentPresenter.kt
@@ -3,6 +3,7 @@ package org.oppia.android.app.classroom
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -36,6 +37,7 @@ import org.oppia.android.app.classroom.promotedlist.PromotedStoryList
import org.oppia.android.app.classroom.topiclist.AllTopicsHeaderText
import org.oppia.android.app.classroom.topiclist.TopicCard
import org.oppia.android.app.classroom.welcome.WelcomeText
+import org.oppia.android.app.home.ExitProfileListener
import org.oppia.android.app.home.HomeItemViewModel
import org.oppia.android.app.home.RouteToTopicPlayStoryListener
import org.oppia.android.app.home.WelcomeViewModel
@@ -48,6 +50,8 @@ import org.oppia.android.app.home.topiclist.TopicSummaryViewModel
import org.oppia.android.app.model.ClassroomSummary
import org.oppia.android.app.model.LessonThumbnail
import org.oppia.android.app.model.LessonThumbnailGraphic
+import org.oppia.android.app.model.Profile
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.TopicSummary
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.app.utility.datetime.DateTimeUtil
@@ -59,9 +63,13 @@ import org.oppia.android.domain.oppialogger.analytics.AnalyticsController
import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.domain.topic.TopicListController
import org.oppia.android.domain.translation.TranslationController
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProviders.Companion.toLiveData
import org.oppia.android.util.locale.OppiaLocale
import org.oppia.android.util.parser.html.StoryHtmlParserEntityType
import org.oppia.android.util.parser.html.TopicHtmlParserEntityType
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
+import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
@@ -82,14 +90,18 @@ class ClassroomListFragmentPresenter @Inject constructor(
private val dateTimeUtil: DateTimeUtil,
private val translationController: TranslationController,
private val machineLocale: OppiaLocale.MachineLocale,
- private val appStartupStateController: AppStartupStateController,
private val analyticsController: AnalyticsController,
+ @EnableOnboardingFlowV2
+ private val enableOnboardingFlowV2: PlatformParameterValue,
+ private val appStartupStateController: AppStartupStateController
) {
private val routeToTopicPlayStoryListener = activity as RouteToTopicPlayStoryListener
+ private val exitProfileListener = activity as ExitProfileListener
private lateinit var binding: ClassroomListFragmentBinding
private lateinit var classroomListViewModel: ClassroomListViewModel
private var internalProfileId: Int = -1
private val profileId = activity.intent.extractCurrentUserProfileId()
+ private var onBackPressedCallback: OnBackPressedCallback? = null
/** Creates and returns the view for the [ClassroomListFragment]. */
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
@@ -155,6 +167,10 @@ class ClassroomListFragmentPresenter @Inject constructor(
}
)
+ profileManagementController.getProfile(profileId).toLiveData().observe(fragment) {
+ processProfileResult(it)
+ }
+
return binding.root
}
@@ -190,26 +206,25 @@ class ClassroomListFragmentPresenter @Inject constructor(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ClassroomListScreen() {
- val groupedItems = classroomListViewModel.homeItemViewModelListLiveData.value
- ?.plus(classroomListViewModel.topicList)
- ?.groupBy { it::class }
+ val groupedItems = (
+ classroomListViewModel.homeItemViewModelListLiveData.value.orEmpty() +
+ classroomListViewModel.topicList
+ )
+ .groupBy { it::class }
val topicListSpanCount = integerResource(id = R.integer.home_span_count)
val listState = rememberLazyListState()
val classroomListIndex = groupedItems
- ?.flatMap { (type, items) -> items.map { type to it } }
- ?.indexOfFirst { it.first == AllClassroomsViewModel::class }
- ?: -1
+ .flatMap { (type, items) -> items.map { type to it } }
+ .indexOfFirst { it.first == AllClassroomsViewModel::class }
LazyColumn(
modifier = Modifier.testTag(CLASSROOM_LIST_SCREEN_TEST_TAG),
state = listState
) {
- groupedItems?.forEach { (type, items) ->
+ groupedItems.forEach { (type, items) ->
when (type) {
WelcomeViewModel::class -> items.forEach { item ->
- item {
- WelcomeText(welcomeViewModel = item as WelcomeViewModel)
- }
+ item { WelcomeText(welcomeViewModel = item as WelcomeViewModel) }
}
PromotedStoryListViewModel::class -> items.forEach { item ->
item {
@@ -223,26 +238,22 @@ class ClassroomListFragmentPresenter @Inject constructor(
item {
ComingSoonTopicList(
comingSoonTopicListViewModel = item as ComingSoonTopicListViewModel,
- machineLocale = machineLocale,
+ machineLocale = machineLocale
)
}
}
AllClassroomsViewModel::class -> items.forEach { _ ->
- item {
- AllClassroomsHeaderText()
- }
+ item { AllClassroomsHeaderText() }
}
- ClassroomSummaryViewModel::class -> stickyHeader() {
+ ClassroomSummaryViewModel::class -> stickyHeader {
ClassroomList(
classroomSummaryList = items.map { it as ClassroomSummaryViewModel },
- selectedClassroomId = classroomListViewModel.selectedClassroomId.get() ?: "",
+ selectedClassroomId = classroomListViewModel.selectedClassroomId.get().orEmpty(),
isSticky = listState.firstVisibleItemIndex >= classroomListIndex
)
}
AllTopicsViewModel::class -> items.forEach { _ ->
- item {
- AllTopicsHeaderText()
- }
+ item { AllTopicsHeaderText() }
}
TopicSummaryViewModel::class -> {
gridItems(
@@ -259,12 +270,61 @@ class ClassroomListFragmentPresenter @Inject constructor(
}
}
+ private fun processProfileResult(result: AsyncResult) {
+ when (result) {
+ is AsyncResult.Success -> {
+ val profile = result.value
+ val profileType = profile.profileType
+
+ if (enableOnboardingFlowV2.value && !profile.completedProfileOnboarding) {
+ // These asynchronous API calls do not block or wait for their results. They execute in
+ // the background and have minimal chances of interfering with the synchronous
+ // `handleBackPress` call below.
+ profileManagementController.markProfileOnboardingEnded(profileId)
+ if (profileType == ProfileType.SOLE_LEARNER || profileType == ProfileType.SUPERVISOR) {
+ appStartupStateController.markOnboardingFlowCompleted(profileId)
+ }
+ }
+
+ // This synchronous function call executes independently of the async calls above.
+ handleBackPress(profileType)
+ }
+ is AsyncResult.Failure -> {
+ oppiaLogger.e(
+ "ClassroomListFragment", "Failed to fetch profile with id:$profileId", result.error
+ )
+ Profile.getDefaultInstance()
+ }
+ is AsyncResult.Pending -> {
+ Profile.getDefaultInstance()
+ }
+ }
+ }
+
private fun logHomeActivityEvent() {
analyticsController.logImportantEvent(
oppiaLogger.createOpenHomeContext(),
profileId
)
}
+
+ private fun handleBackPress(profileType: ProfileType) {
+ onBackPressedCallback?.remove()
+
+ onBackPressedCallback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ exitProfileListener.exitProfile(profileType)
+ // The dispatcher can hold a reference to the host
+ // so we need to null it out to prevent memory leaks.
+ this.remove()
+ onBackPressedCallback = null
+ }
+ }
+
+ onBackPressedCallback?.let { callback ->
+ activity.onBackPressedDispatcher.addCallback(fragment, callback)
+ }
+ }
}
/** Adds a grid of items to a LazyListScope with specified arrangement and item content. */
diff --git a/app/src/main/java/org/oppia/android/app/drawer/ExitProfileDialogFragment.kt b/app/src/main/java/org/oppia/android/app/drawer/ExitProfileDialogFragment.kt
index 2dfdd918fc7..68d389a3ff5 100644
--- a/app/src/main/java/org/oppia/android/app/drawer/ExitProfileDialogFragment.kt
+++ b/app/src/main/java/org/oppia/android/app/drawer/ExitProfileDialogFragment.kt
@@ -13,6 +13,7 @@ import org.oppia.android.app.fragment.FragmentComponentImpl
import org.oppia.android.app.fragment.InjectableDialogFragment
import org.oppia.android.app.model.ExitProfileDialogArguments
import org.oppia.android.app.model.HighlightItem
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.profile.ProfileChooserActivity
import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.extensions.putProto
@@ -63,6 +64,8 @@ class ExitProfileDialogFragment : InjectableDialogFragment() {
else -> false
}
+ val soleLearnerProfile = exitProfileDialogArguments.profileType == ProfileType.SOLE_LEARNER
+
val alertDialog = AlertDialog
.Builder(ContextThemeWrapper(activity as Context, R.style.OppiaAlertDialogTheme))
.setMessage(R.string.home_activity_back_dialog_message)
@@ -70,11 +73,16 @@ class ExitProfileDialogFragment : InjectableDialogFragment() {
dialog.dismiss()
}
.setPositiveButton(R.string.home_activity_back_dialog_exit) { _, _ ->
- val intent = ProfileChooserActivity.createProfileChooserActivity(activity!!)
- if (!restoreLastCheckedItem) {
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ if (soleLearnerProfile) {
+ requireActivity().finish()
+ } else {
+ val intent = ProfileChooserActivity.createProfileChooserActivity(requireActivity())
+ if (!restoreLastCheckedItem) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
+ requireActivity().startActivity(intent)
+ requireActivity().finish()
}
- activity!!.startActivity(intent)
}
.create()
alertDialog.setCanceledOnTouchOutside(false)
diff --git a/app/src/main/java/org/oppia/android/app/home/ExitProfileListener.kt b/app/src/main/java/org/oppia/android/app/home/ExitProfileListener.kt
new file mode 100644
index 00000000000..6b0c0a84480
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/home/ExitProfileListener.kt
@@ -0,0 +1,13 @@
+package org.oppia.android.app.home
+
+import org.oppia.android.app.model.ProfileType
+
+/** Listener for when a user wishes to exit their profile. */
+interface ExitProfileListener {
+ /**
+ * Called when back press is clicked on the HomeScreen.
+ *
+ * Routing behaviour may change based on [ProfileType]
+ */
+ fun exitProfile(profileType: ProfileType)
+}
diff --git a/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt b/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt
index 34885717a33..a0ce5607f6d 100644
--- a/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/home/HomeActivity.kt
@@ -13,12 +13,15 @@ import org.oppia.android.app.model.DestinationScreen
import org.oppia.android.app.model.ExitProfileDialogArguments
import org.oppia.android.app.model.HighlightItem
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.RecentlyPlayedActivityParams
import org.oppia.android.app.model.RecentlyPlayedActivityTitle
import org.oppia.android.app.model.ScreenName.HOME_ACTIVITY
import org.oppia.android.app.topic.TopicActivity
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
+import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
@@ -28,7 +31,8 @@ class HomeActivity :
InjectableAutoLocalizedAppCompatActivity(),
RouteToTopicListener,
RouteToTopicPlayStoryListener,
- RouteToRecentlyPlayedListener {
+ RouteToRecentlyPlayedListener,
+ ExitProfileListener {
@Inject
lateinit var homeActivityPresenter: HomeActivityPresenter
@@ -38,12 +42,15 @@ class HomeActivity :
@Inject
lateinit var activityRouter: ActivityRouter
+ @Inject
+ @field:EnableOnboardingFlowV2
+ lateinit var enableOnboardingFlowV2: PlatformParameterValue
+
private var internalProfileId: Int = -1
companion object {
fun createHomeActivity(context: Context, profileId: ProfileId?): Intent {
-
return Intent(context, HomeActivity::class.java).apply {
decorateWithScreenName(HOME_ACTIVITY)
if (profileId != null) {
@@ -73,22 +80,6 @@ class HomeActivity :
)
}
- override fun onBackPressed() {
- val previousFragment =
- supportFragmentManager.findFragmentByTag(TAG_SWITCH_PROFILE_DIALOG)
- if (previousFragment != null) {
- supportFragmentManager.beginTransaction().remove(previousFragment).commitNow()
- }
- val exitProfileDialogArguments =
- ExitProfileDialogArguments
- .newBuilder()
- .setHighlightItem(HighlightItem.NONE)
- .build()
- val dialogFragment = ExitProfileDialogFragment
- .newInstance(exitProfileDialogArguments = exitProfileDialogArguments)
- dialogFragment.showNow(supportFragmentManager, TAG_SWITCH_PROFILE_DIALOG)
- }
-
override fun routeToTopicPlayStory(
internalProfileId: Int,
classroomId: String,
@@ -120,4 +111,24 @@ class HomeActivity :
.build()
)
}
+
+ override fun exitProfile(profileType: ProfileType) {
+ val previousFragment =
+ supportFragmentManager.findFragmentByTag(TAG_SWITCH_PROFILE_DIALOG)
+ if (previousFragment != null) {
+ supportFragmentManager.beginTransaction().remove(previousFragment).commitNow()
+ }
+ val exitProfileDialogArguments =
+ ExitProfileDialogArguments
+ .newBuilder().apply {
+ if (enableOnboardingFlowV2.value) {
+ this.profileType = profileType
+ }
+ this.highlightItem = HighlightItem.NONE
+ }
+ .build()
+ val dialogFragment = ExitProfileDialogFragment
+ .newInstance(exitProfileDialogArguments = exitProfileDialogArguments)
+ dialogFragment.showNow(supportFragmentManager, TAG_SWITCH_PROFILE_DIALOG)
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt
index 17d41e62f1f..56d1b8cfedc 100644
--- a/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/home/HomeFragmentPresenter.kt
@@ -3,6 +3,7 @@ package org.oppia.android.app.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
@@ -12,7 +13,9 @@ import org.oppia.android.app.home.promotedlist.ComingSoonTopicListViewModel
import org.oppia.android.app.home.promotedlist.PromotedStoryListViewModel
import org.oppia.android.app.home.topiclist.AllTopicsViewModel
import org.oppia.android.app.home.topiclist.TopicSummaryViewModel
+import org.oppia.android.app.model.Profile
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.TopicSummary
import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.app.translation.AppLanguageResourceHandler
@@ -23,13 +26,18 @@ import org.oppia.android.databinding.HomeFragmentBinding
import org.oppia.android.databinding.PromotedStoryListBinding
import org.oppia.android.databinding.TopicSummaryViewBinding
import org.oppia.android.databinding.WelcomeBinding
+import org.oppia.android.domain.onboarding.AppStartupStateController
import org.oppia.android.domain.oppialogger.OppiaLogger
import org.oppia.android.domain.oppialogger.analytics.AnalyticsController
import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.domain.topic.TopicListController
import org.oppia.android.domain.translation.TranslationController
+import org.oppia.android.util.data.AsyncResult
+import org.oppia.android.util.data.DataProviders.Companion.toLiveData
import org.oppia.android.util.parser.html.StoryHtmlParserEntityType
import org.oppia.android.util.parser.html.TopicHtmlParserEntityType
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
+import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
@@ -48,17 +56,24 @@ class HomeFragmentPresenter @Inject constructor(
private val dateTimeUtil: DateTimeUtil,
private val translationController: TranslationController,
private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory,
+ @EnableOnboardingFlowV2
+ private val enableOnboardingFlowV2: PlatformParameterValue,
+ private val appStartupStateController: AppStartupStateController
) {
private val routeToTopicPlayStoryListener = activity as RouteToTopicPlayStoryListener
+ private val exitProfileListener = activity as ExitProfileListener
+
private lateinit var binding: HomeFragmentBinding
private var internalProfileId: Int = -1
+ private var profileId: ProfileId = ProfileId.getDefaultInstance()
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
binding = HomeFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
// NB: Both the view model and lifecycle owner must be set in order to correctly bind LiveData elements to
// data-bound view models.
- internalProfileId = activity.intent.extractCurrentUserProfileId().internalId
+ profileId = activity.intent.extractCurrentUserProfileId()
+ internalProfileId = profileId.internalId
logHomeActivityEvent()
@@ -97,9 +112,42 @@ class HomeFragmentPresenter @Inject constructor(
it.viewModel = homeViewModel
}
+ profileManagementController.getProfile(profileId).toLiveData().observe(fragment) {
+ processProfileResult(it)
+ }
+
return binding.root
}
+ private fun processProfileResult(result: AsyncResult) {
+ when (result) {
+ is AsyncResult.Success -> {
+ val profile = result.value
+ val profileType = profile.profileType
+
+ if (enableOnboardingFlowV2.value && !profile.completedProfileOnboarding) {
+ // These asynchronous API calls do not block or wait for their results. They execute in
+ // the background and have minimal chances of interfering with the synchronous
+ // `handleBackPress` call below.
+ profileManagementController.markProfileOnboardingEnded(profileId)
+ if (profileType == ProfileType.SOLE_LEARNER || profileType == ProfileType.SUPERVISOR) {
+ appStartupStateController.markOnboardingFlowCompleted(profileId)
+ }
+ }
+
+ // This synchronous function call executes independently of the async calls above.
+ handleBackPress(profileType)
+ }
+ is AsyncResult.Failure -> {
+ oppiaLogger.e("HomeFragment", "Failed to fetch profile with id:$profileId", result.error)
+ Profile.getDefaultInstance()
+ }
+ is AsyncResult.Pending -> {
+ Profile.getDefaultInstance()
+ }
+ }
+ }
+
private fun createRecyclerViewAdapter(): BindableAdapter {
return multiTypeBuilderFactory.create { viewModel ->
when (viewModel) {
@@ -167,4 +215,18 @@ class HomeFragmentPresenter @Inject constructor(
ProfileId.newBuilder().apply { internalId = internalProfileId }.build()
)
}
+
+ private fun handleBackPress(profileType: ProfileType) {
+ activity.onBackPressedDispatcher.addCallback(
+ fragment,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ exitProfileListener.exitProfile(profileType)
+ // The dispatcher can hold a reference to the host
+ // so we need to null it out to prevent memory leaks.
+ this.remove()
+ }
+ }
+ )
+ }
}
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
index 43ac0698801..dc16140ffe7 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/AudioLanguageFragmentPresenter.kt
@@ -11,6 +11,7 @@ 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.classroom.ClassroomListActivity
import org.oppia.android.app.home.HomeActivity
import org.oppia.android.app.model.AudioLanguageFragmentStateBundle
import org.oppia.android.app.model.AudioTranslationLanguageSelection
@@ -21,11 +22,14 @@ import org.oppia.android.app.options.AudioLanguageSelectionViewModel
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.databinding.AudioLanguageSelectionFragmentBinding
import org.oppia.android.domain.oppialogger.OppiaLogger
+import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.domain.translation.TranslationController
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
import org.oppia.android.util.extensions.getProto
import org.oppia.android.util.extensions.putProto
+import org.oppia.android.util.platformparameter.EnableMultipleClassrooms
+import org.oppia.android.util.platformparameter.PlatformParameterValue
import javax.inject.Inject
/** The presenter for [AudioLanguageFragment]. */
@@ -34,7 +38,9 @@ class AudioLanguageFragmentPresenter @Inject constructor(
private val activity: AppCompatActivity,
private val appLanguageResourceHandler: AppLanguageResourceHandler,
private val audioLanguageSelectionViewModel: AudioLanguageSelectionViewModel,
+ private val profileManagementController: ProfileManagementController,
private val translationController: TranslationController,
+ @EnableMultipleClassrooms private val enableMultipleClassrooms: PlatformParameterValue,
private val oppiaLogger: OppiaLogger
) {
private lateinit var binding: AudioLanguageSelectionFragmentBinding
@@ -118,12 +124,7 @@ class AudioLanguageFragmentPresenter @Inject constructor(
binding.onboardingNavigationContinue.setOnClickListener {
updateSelectedAudioLanguage(selectedLanguage, profileId).also {
- val intent = HomeActivity.createHomeActivity(fragment.requireContext(), profileId)
- fragment.startActivity(intent)
- // Finish this activity as well as all activities immediately below it in the current
- // task so that the user cannot navigate back to the onboarding flow by pressing the
- // back button once onboarding is complete
- fragment.activity?.finishAffinity()
+ logInToProfile(profileId)
}
}
@@ -159,6 +160,30 @@ class AudioLanguageFragmentPresenter @Inject constructor(
}
}
+ private fun logInToProfile(profileId: ProfileId) {
+ profileManagementController.loginToProfile(profileId).toLiveData().observe(
+ fragment,
+ { result ->
+ if (result is AsyncResult.Success) {
+ navigateToHomeScreen(profileId)
+ }
+ }
+ )
+ }
+
+ private fun navigateToHomeScreen(profileId: ProfileId) {
+ val intent = if (enableMultipleClassrooms.value) {
+ ClassroomListActivity.createClassroomListActivity(fragment.requireContext(), profileId)
+ } else {
+ HomeActivity.createHomeActivity(fragment.requireContext(), profileId)
+ }
+ fragment.startActivity(intent)
+ // Finish this activity as well as all activities immediately below it in the current
+ // task so that the user cannot navigate back to the onboarding flow by pressing the
+ // back button once onboarding is complete.
+ fragment.activity?.finishAffinity()
+ }
+
/** Save the current dropdown selection to be retrieved on configuration change. */
fun handleSavedState(outState: Bundle) {
outState.putProto(
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
index 86f4d548a49..5672eca455f 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
@@ -15,7 +15,7 @@ import javax.inject.Inject
/** Argument key for [CreateProfileFragment] arguments. */
const val CREATE_PROFILE_FRAGMENT_ARGS = "CreateProfileFragment.args"
-private const val TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT = "TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT"
+private const val TAG_CREATE_PROFILE_FRAGMENT = "TAG_CREATE_PROFILE_FRAGMENT"
/** Presenter for [CreateProfileActivity]. */
class CreateProfileActivityPresenter @Inject constructor(
@@ -45,14 +45,14 @@ class CreateProfileActivityPresenter @Inject constructor(
activity.supportFragmentManager.beginTransaction().add(
R.id.profile_fragment_placeholder,
createLearnerProfileFragment,
- TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT
+ TAG_CREATE_PROFILE_FRAGMENT
).commitNow()
}
}
private fun getNewLearnerProfileFragment(): CreateProfileFragment? {
return activity.supportFragmentManager.findFragmentByTag(
- TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT
+ TAG_CREATE_PROFILE_FRAGMENT
) as? CreateProfileFragment
}
}
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 ac7739d5ad3..d4a6a5fdcad 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
@@ -11,6 +11,7 @@ import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.options.AudioLanguageActivity
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.databinding.LearnerIntroFragmentBinding
+import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
@@ -19,10 +20,11 @@ class IntroFragmentPresenter @Inject constructor(
private var fragment: Fragment,
private val activity: AppCompatActivity,
private val appLanguageResourceHandler: AppLanguageResourceHandler,
+ private val profileManagementController: ProfileManagementController,
) {
private lateinit var binding: LearnerIntroFragmentBinding
- /** Handle creation and binding of the OnboardingLearnerIntroFragment layout. */
+ /** Handle creation and binding of the OnboardingLearnerIntroFragment layout. */
fun handleCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -39,6 +41,8 @@ class IntroFragmentPresenter @Inject constructor(
setLearnerName(profileNickname)
+ profileManagementController.markProfileOnboardingStarted(profileId)
+
binding.onboardingNavigationBack.setOnClickListener {
activity.finish()
}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt
index 332fd930117..4fa1645738e 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingFragmentPresenter.kt
@@ -251,8 +251,7 @@ class OnboardingFragmentPresenter @Inject constructor(
private fun createDefaultProfile() {
profileManagementController.addProfile(
- name = "Admin", // TODO(#4938): Refactor to empty name once proper admin profile creation flow
- // is implemented.
+ name = "",
pin = "",
avatarImagePath = null,
allowDownloadAccess = true,
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 5d8a7734007..49be136a69c 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
@@ -6,10 +6,12 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import org.oppia.android.app.model.CreateProfileActivityParams
+import org.oppia.android.app.model.ProfileChooserActivityParams
import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.profile.ProfileChooserActivity
import org.oppia.android.databinding.OnboardingProfileTypeFragmentBinding
+import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
@@ -17,10 +19,14 @@ import javax.inject.Inject
/** Argument key for [CreateProfileActivity] intent parameters. */
const val CREATE_PROFILE_PARAMS_KEY = "CreateProfileActivity.params"
+/** Argument key for [ProfileChooserActivity] intent parameters. */
+const val PROFILE_CHOOSER_PARAMS_KEY = "ProfileChooserActivity.params"
+
/** The presenter for [OnboardingProfileTypeFragment]. */
class OnboardingProfileTypeFragmentPresenter @Inject constructor(
private val fragment: Fragment,
- private val activity: AppCompatActivity
+ private val activity: AppCompatActivity,
+ private val profileManagementController: ProfileManagementController
) {
private lateinit var binding: OnboardingProfileTypeFragmentBinding
@@ -54,9 +60,22 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
}
profileTypeSupervisorNavigationCard.setOnClickListener {
+ // TODO(#4938): Remove once admin profile onboarding is implemented.
+ profileManagementController.markProfileOnboardingStarted(profileId)
+
val intent = ProfileChooserActivity.createProfileChooserActivity(activity)
- // TODO(#4938): Add profileId and ProfileType to intent extras.
+ intent.apply {
+ decorateWithUserProfileId(profileId)
+ putProtoExtra(
+ PROFILE_CHOOSER_PARAMS_KEY,
+ ProfileChooserActivityParams.newBuilder()
+ .setProfileType(ProfileType.SUPERVISOR)
+ .build()
+ )
+ }
fragment.startActivity(intent)
+ // Clear back stack so that user cannot go back to the onboarding flow.
+ fragment.activity?.finishAffinity()
}
onboardingNavigationBack.setOnClickListener {
diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt
index 3d16b36ef84..4a19c0f74dd 100644
--- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivity.kt
@@ -5,8 +5,12 @@ import android.content.Intent
import android.os.Bundle
import org.oppia.android.app.activity.ActivityComponentImpl
import org.oppia.android.app.activity.InjectableSystemLocalizedAppCompatActivity
+import org.oppia.android.app.model.ProfileChooserActivityParams
import org.oppia.android.app.model.ScreenName.PROFILE_CHOOSER_ACTIVITY
+import org.oppia.android.app.onboarding.PROFILE_CHOOSER_PARAMS_KEY
+import org.oppia.android.util.extensions.getProtoExtra
import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId
import javax.inject.Inject
/** Activity that controls profile creation and selection. */
@@ -26,6 +30,14 @@ class ProfileChooserActivity : InjectableSystemLocalizedAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
- profileChooserActivityPresenter.handleOnCreate()
+
+ val profileType = intent.getProtoExtra(
+ PROFILE_CHOOSER_PARAMS_KEY,
+ ProfileChooserActivityParams.getDefaultInstance()
+ ).profileType
+
+ val profileId = intent.extractCurrentUserProfileId()
+
+ profileChooserActivityPresenter.handleOnCreate(profileId, profileType)
}
}
diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivityPresenter.kt
index a61009bb979..6bfe0bb3122 100644
--- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserActivityPresenter.kt
@@ -3,27 +3,46 @@ package org.oppia.android.app.profile
import androidx.appcompat.app.AppCompatActivity
import org.oppia.android.R
import org.oppia.android.app.activity.ActivityScope
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.testing.ProfileChooserFragmentTestActivity
import org.oppia.android.domain.profile.ProfileManagementController
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
+import org.oppia.android.util.platformparameter.PlatformParameterValue
import javax.inject.Inject
/** The presenter for [ProfileChooserActivity]. */
@ActivityScope
class ProfileChooserActivityPresenter @Inject constructor(
private val activity: AppCompatActivity,
- private val profileManagementController: ProfileManagementController
+ private val profileManagementController: ProfileManagementController,
+ @EnableOnboardingFlowV2
+ private val enableOnboardingFlowV2: PlatformParameterValue
) {
/** Adds [ProfileChooserFragment] to view. */
- fun handleOnCreate() {
- // TODO(#482): Ensures that an admin profile is present. Remove when there is proper admin account creation.
- profileManagementController.addProfile(
- name = "Admin",
- pin = "",
- avatarImagePath = null,
- allowDownloadAccess = true,
- colorRgb = -10710042,
- isAdmin = true
- )
+ fun handleOnCreate(profileId: ProfileId, profileType: ProfileType) {
+ if (enableOnboardingFlowV2.value) {
+ profileManagementController.updateNewProfileDetails(
+ profileId = profileId,
+ profileType = profileType,
+ newName = "Admin",
+ avatarImagePath = null,
+ colorRgb = -10710042,
+ isAdmin = true
+ )
+ } else {
+ // TODO(#482): Ensures that an admin profile is present.
+ // This can be removed once the new onboarding flow is finalized, as it will handle the creation of an admin profile.
+ profileManagementController.addProfile(
+ name = "Admin",
+ pin = "",
+ avatarImagePath = null,
+ allowDownloadAccess = true,
+ colorRgb = -10710042,
+ isAdmin = true
+ )
+ }
+
activity.setContentView(R.layout.profile_chooser_activity)
if (getProfileChooserFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt
index 371bdfc9037..2cab08277ff 100644
--- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt
@@ -17,9 +17,12 @@ import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity
import org.oppia.android.app.classroom.ClassroomListActivity
import org.oppia.android.app.fragment.FragmentScope
import org.oppia.android.app.home.HomeActivity
+import org.oppia.android.app.model.IntroActivityParams
import org.oppia.android.app.model.Profile
import org.oppia.android.app.model.ProfileChooserUiModel
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
+import org.oppia.android.app.onboarding.IntroActivity
import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.databinding.ProfileChooserAddViewBinding
import org.oppia.android.databinding.ProfileChooserFragmentBinding
@@ -29,8 +32,11 @@ import org.oppia.android.domain.oppialogger.analytics.AnalyticsController
import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
+import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.platformparameter.EnableMultipleClassrooms
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import org.oppia.android.util.statusbar.StatusBarColor
import javax.inject.Inject
@@ -73,6 +79,7 @@ class ProfileChooserFragmentPresenter @Inject constructor(
private val analyticsController: AnalyticsController,
private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory,
@EnableMultipleClassrooms private val enableMultipleClassrooms: PlatformParameterValue,
+ @EnableOnboardingFlowV2 private val enableOnboardingFlowV2: PlatformParameterValue,
) {
private lateinit var binding: ProfileChooserFragmentBinding
val hasProfileEverBeenAddedValue = ObservableField(true)
@@ -174,30 +181,10 @@ class ProfileChooserFragmentPresenter @Inject constructor(
binding.hasProfileEverBeenAddedValue = hasProfileEverBeenAddedValue
binding.profileChooserItem.setOnClickListener {
updateLearnerIdIfAbsent(model.profile)
- if (model.profile.pin.isEmpty()) {
- profileManagementController.loginToProfile(model.profile.id).toLiveData().observe(
- fragment,
- Observer {
- if (it is AsyncResult.Success) {
- if (enableMultipleClassrooms.value) {
- activity.startActivity(
- ClassroomListActivity.createClassroomListActivity(activity, model.profile.id)
- )
- } else {
- activity.startActivity(
- HomeActivity.createHomeActivity(activity, model.profile.id)
- )
- }
- }
- }
- )
+ if (enableOnboardingFlowV2.value) {
+ ensureProfileOnboarded(model.profile)
} else {
- val pinPasswordIntent = PinPasswordActivity.createPinPasswordActivityIntent(
- activity,
- chooserViewModel.adminPin,
- model.profile.id.internalId
- )
- activity.startActivity(pinPasswordIntent)
+ logInToProfile(model.profile)
}
}
}
@@ -267,4 +254,54 @@ class ProfileChooserFragmentPresenter @Inject constructor(
profileManagementController.initializeLearnerId(profile.id)
}
}
+
+ private fun ensureProfileOnboarded(profile: Profile) {
+ if (profile.profileType == ProfileType.SUPERVISOR || profile.completedProfileOnboarding) {
+ logInToProfile(profile)
+ } else {
+ launchOnboardingScreen(profile.id, profile.name)
+ }
+ }
+
+ private fun launchOnboardingScreen(profileId: ProfileId, profileName: String) {
+ val introActivityParams = IntroActivityParams.newBuilder()
+ .setProfileNickname(profileName)
+ .build()
+
+ val intent = IntroActivity.createIntroActivity(activity)
+ intent.apply {
+ putProtoExtra(IntroActivity.PARAMS_KEY, introActivityParams)
+ decorateWithUserProfileId(profileId)
+ }
+
+ activity.startActivity(intent)
+ }
+
+ private fun logInToProfile(profile: Profile) {
+ if (profile.pin.isNullOrBlank()) {
+ profileManagementController.loginToProfile(profile.id).toLiveData().observe(
+ fragment,
+ {
+ if (it is AsyncResult.Success) {
+ if (enableMultipleClassrooms.value) {
+ activity.startActivity(
+ ClassroomListActivity.createClassroomListActivity(activity, profile.id)
+ )
+ } else {
+ activity.startActivity(
+ HomeActivity.createHomeActivity(activity, profile.id)
+ )
+ }
+ }
+ }
+ )
+ } else {
+ val pinPasswordIntent = PinPasswordActivity.createPinPasswordActivityIntent(
+ activity,
+ chooserViewModel.adminPin,
+ profile.id.internalId
+ )
+ activity.startActivity(pinPasswordIntent)
+ }
+ }
}
diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt
index 3e2f8254d5d..68d133d07f4 100644
--- a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt
@@ -9,12 +9,19 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import org.oppia.android.R
import org.oppia.android.app.activity.ActivityScope
+import org.oppia.android.app.classroom.ClassroomListActivity
+import org.oppia.android.app.home.HomeActivity
import org.oppia.android.app.model.AppStartupState
import org.oppia.android.app.model.AppStartupState.BuildFlavorNoticeMode
import org.oppia.android.app.model.AppStartupState.StartupMode
import org.oppia.android.app.model.BuildFlavor
import org.oppia.android.app.model.DeprecationNoticeType
import org.oppia.android.app.model.DeprecationResponse
+import org.oppia.android.app.model.IntroActivityParams
+import org.oppia.android.app.model.Profile
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileOnboardingMode
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.notice.AutomaticAppDeprecationNoticeDialogFragment
import org.oppia.android.app.notice.BetaNoticeDialogFragment
import org.oppia.android.app.notice.DeprecationNoticeActionResponse
@@ -22,6 +29,8 @@ import org.oppia.android.app.notice.ForcedAppDeprecationNoticeDialogFragment
import org.oppia.android.app.notice.GeneralAvailabilityUpgradeNoticeDialogFragment
import org.oppia.android.app.notice.OptionalAppDeprecationNoticeDialogFragment
import org.oppia.android.app.notice.OsDeprecationNoticeDialogFragment
+import org.oppia.android.app.onboarding.IntroActivity
+import org.oppia.android.app.onboarding.IntroActivity.Companion.PARAMS_KEY
import org.oppia.android.app.onboarding.OnboardingActivity
import org.oppia.android.app.profile.ProfileChooserActivity
import org.oppia.android.app.translation.AppLanguageLocaleHandler
@@ -31,14 +40,19 @@ import org.oppia.android.domain.locale.LocaleController
import org.oppia.android.domain.onboarding.AppStartupStateController
import org.oppia.android.domain.onboarding.DeprecationController
import org.oppia.android.domain.oppialogger.OppiaLogger
+import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.domain.translation.TranslationController
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.data.DataProvider
import org.oppia.android.util.data.DataProviders.Companion.combineWith
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
+import org.oppia.android.util.extensions.putProtoExtra
import org.oppia.android.util.locale.OppiaLocale
import org.oppia.android.util.platformparameter.EnableAppAndOsDeprecation
+import org.oppia.android.util.platformparameter.EnableMultipleClassrooms
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import javax.inject.Inject
private const val AUTO_DEPRECATION_NOTICE_DIALOG_FRAGMENT_TAG = "auto_deprecation_notice_dialog"
@@ -63,6 +77,9 @@ class SplashActivityPresenter @Inject constructor(
private val currentBuildFlavor: BuildFlavor,
@EnableAppAndOsDeprecation
private val enableAppAndOsDeprecation: PlatformParameterValue,
+ private val profileManagementController: ProfileManagementController,
+ @EnableOnboardingFlowV2 private val enableOnboardingFlowV2: PlatformParameterValue,
+ @EnableMultipleClassrooms private val enableMultipleClassrooms: PlatformParameterValue
) {
lateinit var startupMode: StartupMode
@@ -243,10 +260,7 @@ class SplashActivityPresenter @Inject constructor(
private fun processAppAndOsDeprecationEnabledStartUpMode() {
when (startupMode) {
- StartupMode.USER_IS_ONBOARDED -> {
- activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity))
- activity.finish()
- }
+ StartupMode.USER_IS_ONBOARDED -> handleUserOnboarded()
StartupMode.APP_IS_DEPRECATED -> {
showDialog(
FORCED_DEPRECATION_NOTICE_DIALOG_FRAGMENT_TAG,
@@ -265,10 +279,11 @@ class SplashActivityPresenter @Inject constructor(
OsDeprecationNoticeDialogFragment::newInstance
)
}
+ StartupMode.USER_NOT_YET_ONBOARDED -> fetchProfile()
else -> {
// In all other cases (including errors when the startup state fails to load or is
// defaulted), assume the user needs to be onboarded.
- activity.startActivity(OnboardingActivity.createOnboardingActivity(activity))
+ launchOnboardingActivity()
activity.finish()
}
}
@@ -276,25 +291,142 @@ class SplashActivityPresenter @Inject constructor(
private fun processLegacyStartupMode() {
when (startupMode) {
- StartupMode.USER_IS_ONBOARDED -> {
- activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity))
- activity.finish()
- }
+ StartupMode.USER_IS_ONBOARDED -> handleUserOnboarded()
StartupMode.APP_IS_DEPRECATED -> {
showDialog(
AUTO_DEPRECATION_NOTICE_DIALOG_FRAGMENT_TAG,
AutomaticAppDeprecationNoticeDialogFragment::newInstance
)
}
+ StartupMode.USER_NOT_YET_ONBOARDED -> fetchProfile()
else -> {
// In all other cases (including errors when the startup state fails to load or is
// defaulted), assume the user needs to be onboarded.
- activity.startActivity(OnboardingActivity.createOnboardingActivity(activity))
+ launchOnboardingActivity()
+ }
+ }
+ }
+
+ private fun handleUserOnboarded() {
+ if (enableOnboardingFlowV2.value) {
+ getProfileOnboardingState()
+ } else {
+ activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity))
+ activity.finish()
+ }
+ }
+
+ private fun getProfileOnboardingState() {
+ profileManagementController.getProfileOnboardingMode().toLiveData().observe(
+ activity,
+ { result ->
+ when (result) {
+ is AsyncResult.Success -> computeLoginRoute(result.value)
+ is AsyncResult.Failure -> oppiaLogger.e(
+ "SplashActivity",
+ "Encountered unexpected non-successful result when fetching onboarding state",
+ result.error
+ )
+ is AsyncResult.Pending -> {}
+ }
+ }
+ )
+ }
+
+ private fun computeLoginRoute(onboardingMode: ProfileOnboardingMode) {
+ when (onboardingMode) {
+ ProfileOnboardingMode.NEW_INSTALL -> {
+ launchOnboardingActivity()
+ }
+ ProfileOnboardingMode.SOLE_LEARNER_PROFILE_ONLY -> fetchProfile()
+ else -> {
+ activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity))
activity.finish()
}
}
}
+ private fun fetchProfile() {
+ val liveData = profileManagementController.getProfiles().toLiveData()
+ liveData.observe(
+ activity,
+ object : Observer>> {
+ override fun onChanged(result: AsyncResult>) {
+ when (result) {
+ is AsyncResult.Success -> {
+ handleProfiles(result.value)
+ // Changes to underlying DataProviders will update the profiles result,
+ // causing an infinite login loop. At this point we are not interested in further
+ // updates to the profiles DataProvider.
+ liveData.removeObserver(this)
+ }
+ is AsyncResult.Failure -> oppiaLogger.e(
+ "SplashActivity", "Failed to retrieve the list of profiles", result.error
+ )
+ is AsyncResult.Pending -> {} // no-op
+ }
+ }
+ }
+ )
+ }
+
+ private fun handleProfiles(profiles: List) {
+ val soleLearnerProfile = profiles.find { it.profileType == ProfileType.SOLE_LEARNER }
+ if (soleLearnerProfile != null) {
+ proceedBasedOnProfileState(soleLearnerProfile)
+ } else {
+ launchOnboardingActivity()
+ }
+ }
+
+ private fun proceedBasedOnProfileState(profile: Profile) {
+ when {
+ profile.startedProfileOnboarding && !profile.completedProfileOnboarding -> {
+ resumeOnboarding(profile.id, profile.name)
+ }
+ profile.startedProfileOnboarding && profile.completedProfileOnboarding -> {
+ logInToProfile(profile.id)
+ }
+ else -> launchOnboardingActivity()
+ }
+ }
+
+ private fun resumeOnboarding(profileId: ProfileId, profileName: String) {
+ val introActivityParams = IntroActivityParams.newBuilder()
+ .setProfileNickname(profileName)
+ .build()
+
+ val intent = IntroActivity.createIntroActivity(activity).apply {
+ putProtoExtra(PARAMS_KEY, introActivityParams)
+ decorateWithUserProfileId(profileId)
+ }
+
+ activity.startActivity(intent)
+ }
+
+ private fun logInToProfile(profileId: ProfileId) {
+ profileManagementController.loginToProfile(profileId).toLiveData().observe(activity) { result ->
+ if (result is AsyncResult.Success && !activity.isFinishing) {
+ launchHomeScreen(profileId)
+ }
+ }
+ }
+
+ private fun launchHomeScreen(profileId: ProfileId) {
+ val intent = if (enableMultipleClassrooms.value) {
+ ClassroomListActivity.createClassroomListActivity(activity, profileId)
+ } else {
+ HomeActivity.createHomeActivity(activity, profileId)
+ }
+ activity.startActivity(intent)
+ activity.finish()
+ }
+
+ private fun launchOnboardingActivity() {
+ activity.startActivity(OnboardingActivity.createOnboardingActivity(activity))
+ activity.finish()
+ }
+
private fun computeInitStateDataProvider(): DataProvider {
val startupStateDataProvider = appStartupStateController.getAppStartupState()
val systemAppLanguageLocaleDataProvider = translationController.getSystemLanguageLocale()
diff --git a/app/src/main/java/org/oppia/android/app/testing/HomeFragmentTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/HomeFragmentTestActivity.kt
index 20731d36f2f..fc90e80c471 100644
--- a/app/src/main/java/org/oppia/android/app/testing/HomeFragmentTestActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/testing/HomeFragmentTestActivity.kt
@@ -4,10 +4,12 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import org.oppia.android.app.activity.ActivityComponentImpl
+import org.oppia.android.app.home.ExitProfileListener
import org.oppia.android.app.home.HomeFragment
import org.oppia.android.app.home.RouteToRecentlyPlayedListener
import org.oppia.android.app.home.RouteToTopicListener
import org.oppia.android.app.home.RouteToTopicPlayStoryListener
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.RecentlyPlayedActivityTitle
import org.oppia.android.app.testing.activity.TestActivity
@@ -19,7 +21,8 @@ class HomeFragmentTestActivity :
RouteToTopicListener,
RouteToTopicPlayStoryListener,
RouteToRecentlyPlayedListener,
- TestActivity() {
+ TestActivity(),
+ ExitProfileListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -41,4 +44,5 @@ class HomeFragmentTestActivity :
storyId: String
) {}
override fun routeToRecentlyPlayed(recentlyPlayedActivityTitle: RecentlyPlayedActivityTitle) {}
+ override fun exitProfile(profileType: ProfileType) {}
}
diff --git a/app/src/main/java/org/oppia/android/app/testing/NavigationDrawerTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/NavigationDrawerTestActivity.kt
index 6097b74d8b5..57e9f72bc8b 100644
--- a/app/src/main/java/org/oppia/android/app/testing/NavigationDrawerTestActivity.kt
+++ b/app/src/main/java/org/oppia/android/app/testing/NavigationDrawerTestActivity.kt
@@ -7,12 +7,14 @@ import org.oppia.android.R
import org.oppia.android.app.activity.ActivityComponentImpl
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
import org.oppia.android.app.activity.route.ActivityRouter
+import org.oppia.android.app.home.ExitProfileListener
import org.oppia.android.app.home.HomeActivityPresenter
import org.oppia.android.app.home.RouteToRecentlyPlayedListener
import org.oppia.android.app.home.RouteToTopicListener
import org.oppia.android.app.home.RouteToTopicPlayStoryListener
import org.oppia.android.app.model.DestinationScreen
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.RecentlyPlayedActivityParams
import org.oppia.android.app.model.RecentlyPlayedActivityTitle
import org.oppia.android.app.topic.TopicActivity
@@ -25,7 +27,8 @@ class NavigationDrawerTestActivity :
InjectableAutoLocalizedAppCompatActivity(),
RouteToTopicListener,
RouteToTopicPlayStoryListener,
- RouteToRecentlyPlayedListener {
+ RouteToRecentlyPlayedListener,
+ ExitProfileListener {
@Inject
lateinit var homeActivityPresenter: HomeActivityPresenter
@@ -99,4 +102,6 @@ class NavigationDrawerTestActivity :
.build()
)
}
+
+ override fun exitProfile(profileType: ProfileType) {}
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt
index 9a9e863143b..45fde0ccbfd 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/classroom/ClassroomListFragmentTest.kt
@@ -6,12 +6,13 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.performScrollToNode
+import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
@@ -20,9 +21,9 @@ import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.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
@@ -48,7 +49,12 @@ import org.oppia.android.app.classroom.welcome.WELCOME_TEST_TAG
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.home.recentlyplayed.RecentlyPlayedActivity
+import org.oppia.android.app.model.EventLog
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.COMPLETE_APP_ONBOARDING
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.END_PROFILE_ONBOARDING_EVENT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_HOME
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.TopicActivityParams
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
@@ -78,6 +84,7 @@ 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.AppStartupStateController
import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.android.domain.oppialogger.LogStorageModule
import org.oppia.android.domain.oppialogger.LoggingIdentifierModule
@@ -92,6 +99,7 @@ import org.oppia.android.domain.topic.FRACTIONS_TOPIC_ID
import org.oppia.android.domain.topic.TEST_STORY_ID_0
import org.oppia.android.domain.topic.TEST_TOPIC_ID_0
import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.FakeAnalyticsEventLogger
import org.oppia.android.testing.OppiaTestRule
import org.oppia.android.testing.TestImageLoaderModule
import org.oppia.android.testing.TestLogReportingModule
@@ -150,7 +158,7 @@ class ClassroomListFragmentTest {
val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
@get:Rule
- val composeRule = createAndroidComposeRule()
+ val composeRule = createEmptyComposeRule()
@Inject
lateinit var context: Context
@@ -173,26 +181,164 @@ class ClassroomListFragmentTest {
@Inject
lateinit var dataProviderTestMonitor: DataProviderTestMonitor.Factory
- private val internalProfileId: Int = 0
- private lateinit var profileId: ProfileId
+ @Inject
+ lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger
- @Before
- fun setUp() {
- Intents.init()
- setUpTestApplicationComponent()
- profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build()
- testCoroutineDispatchers.registerIdlingResource()
- profileTestHelper.initializeProfiles()
- }
+ private lateinit var scenario: ActivityScenario
+
+ private val internalProfileId: Int = 0
+ private val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build()
@After
fun tearDown() {
+ TestPlatformParameterModule.reset()
testCoroutineDispatchers.unregisterIdlingResource()
- Intents.release()
+ scenario.close()
+ }
+
+ @Test
+ fun testFragment_onboardingV1Enabled_onLaunch_logsOpenHomeEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = false)
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+ val event = fakeAnalyticsEventLogger.getOldestEvent()
+
+ assertThat(event.priority).isEqualTo(EventLog.Priority.ESSENTIAL)
+ assertThat(event.context.activityContextCase).isEqualTo(OPEN_HOME)
+ }
+
+ @Test
+ fun testFragment_onboardingV2Enabled_onLaunch_logsOpenHomeEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = true)
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
+ val event = fakeAnalyticsEventLogger.getOldestEvent()
+
+ assertThat(event.priority).isEqualTo(EventLog.Priority.ESSENTIAL)
+ assertThat(event.context.activityContextCase).isEqualTo(OPEN_HOME)
+ }
+
+ @Test
+ fun testFragment_onboardingV2_soleLearner_onInitialLaunch_logsEndProfileOnboardingEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = true)
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId, profileType = ProfileType.SOLE_LEARNER
+ )
+
+ val profileOnboardingEndedEvent = fakeAnalyticsEventLogger.getMostRecentEvent()
+
+ assertThat(profileOnboardingEndedEvent.priority).isEqualTo(EventLog.Priority.OPTIONAL)
+ assertThat(profileOnboardingEndedEvent.context.activityContextCase)
+ .isEqualTo(END_PROFILE_ONBOARDING_EVENT)
+ }
+
+ @Test
+ fun testFragment_onboardingV2_supervisorProfile_onInitialLaunch_logsEndProfileOnboardingEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = true)
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId, profileType = ProfileType.SUPERVISOR
+ )
+
+ val profileOnboardingEndedEvent = fakeAnalyticsEventLogger.getMostRecentEvent()
+
+ assertThat(profileOnboardingEndedEvent.priority).isEqualTo(EventLog.Priority.OPTIONAL)
+ assertThat(profileOnboardingEndedEvent.context.activityContextCase)
+ .isEqualTo(END_PROFILE_ONBOARDING_EVENT)
+ }
+
+ @Test
+ fun testFragment_onboardingV2_nonAdminProfile_onInitialLaunch_logsEndProfileOnboardingEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = true)
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId, profileType = ProfileType.ADDITIONAL_LEARNER
+ )
+
+ val profileOnboardingEndedEvent = fakeAnalyticsEventLogger.getMostRecentEvent()
+
+ assertThat(profileOnboardingEndedEvent.priority).isEqualTo(EventLog.Priority.OPTIONAL)
+ assertThat(profileOnboardingEndedEvent.context.activityContextCase)
+ .isEqualTo(END_PROFILE_ONBOARDING_EVENT)
+ }
+
+ @Test
+ fun testFragment_onboardingV2_soleLearner_onInitialLaunch_logsAppOnboardingEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = true)
+
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId, profileType = ProfileType.SOLE_LEARNER
+ )
+ profileTestHelper.markProfileOnboardingStarted(profileId)
+
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+
+ testCoroutineDispatchers.runCurrent()
+
+ val hasAppOnboardingEvent = fakeAnalyticsEventLogger.hasEventLogged {
+ it.context.activityContextCase == COMPLETE_APP_ONBOARDING
+ }
+
+ assertThat(hasAppOnboardingEvent).isTrue()
+ }
+
+ @Test
+ fun testFragment_onboardingV2_supervisorProfile_onInitialLaunch_logsAppOnboardingEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = true)
+
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId, profileType = ProfileType.SUPERVISOR
+ )
+ profileTestHelper.markProfileOnboardingStarted(profileId)
+
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+
+ testCoroutineDispatchers.runCurrent()
+
+ val hasAppOnboardingEvent = fakeAnalyticsEventLogger.hasEventLogged {
+ it.context.activityContextCase == COMPLETE_APP_ONBOARDING
+ }
+
+ assertThat(hasAppOnboardingEvent).isTrue()
+ }
+
+ @Test
+ fun testFragment_onboardingV2_nonAdmin_onInitialLaunch_doesNotLogAppOnboardingEvent() {
+ setUpTestApplicationComponent(onboardingV2Enabled = true)
+
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId, profileType = ProfileType.ADDITIONAL_LEARNER
+ )
+ profileTestHelper.markProfileOnboardingStarted(profileId)
+
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+
+ testCoroutineDispatchers.runCurrent()
+
+ val hasAppOnboardingEvent = fakeAnalyticsEventLogger.hasEventLogged {
+ it.context.activityContextCase == COMPLETE_APP_ONBOARDING
+ }
+
+ assertThat(hasAppOnboardingEvent).isFalse()
}
@Test
fun testFragment_allComponentsAreDisplayed() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(WELCOME_TEST_TAG).assertIsDisplayed()
composeRule.onNodeWithTag(ALL_CLASSROOMS_HEADER_TEST_TAG).assertIsDisplayed()
composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).assertIsDisplayed()
@@ -201,7 +347,11 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_loginTwice_allComponentsAreDisplayed() {
+ setUpTestApplicationComponent()
logIntoAdminTwice()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(WELCOME_TEST_TAG).assertIsDisplayed()
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).assertIsDisplayed()
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).assertIsDisplayed()
@@ -216,13 +366,17 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_withAdminProfile_configChange_profileNameIsDisplayed() {
+ setUpTestApplicationComponent()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP)
// Refresh the welcome text content.
logIntoAdmin()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
composeRule.onNodeWithTag(WELCOME_TEST_TAG)
.assertTextContains("Good evening, Admin!")
@@ -231,6 +385,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_morningTimestamp_goodMorningMessageIsDisplayed_withAdminProfileName() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(MORNING_TIMESTAMP)
@@ -244,6 +400,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_afternoonTimestamp_goodAfternoonMessageIsDisplayed_withAdminProfileName() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(AFTERNOON_TIMESTAMP)
@@ -257,6 +415,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_eveningTimestamp_goodEveningMessageIsDisplayed_withAdminProfileName() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP)
@@ -270,12 +430,16 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_logUserInFirstTime_checkPromotedStoriesIsNotDisplayed() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).assertDoesNotExist()
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).assertDoesNotExist()
}
@Test
fun testFragment_recentlyPlayedStoriesTextIsDisplayed() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
@@ -294,6 +458,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_viewAllTextIsDisplayed() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
@@ -318,6 +484,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_storiesPlayedOneWeekAgo_displaysLastPlayedStoriesText() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
@@ -337,6 +505,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_markStory0DoneForFraction_displaysRecommendedStories() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedFractionsTopic(
@@ -365,6 +535,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_markCompletedRatiosStory0_recommendsFractions() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedRatiosStory0(
@@ -385,6 +557,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_noTopicProgress_initialRecommendationFractionsAndRatiosIsCorrect() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0)
@@ -408,19 +582,25 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_forPromotedActivityList_hideViewAll() {
- logIntoAdminTwice()
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId,
timestampOlderThanOneWeek = false
)
+ logIntoAdminTwice()
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(1)
.assertDoesNotExist()
}
@Test
fun testFragment_markStory0DoneForRatiosAndFirstTestTopic_displaysSuggestedStories() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic0Story0(
@@ -463,6 +643,8 @@ class ClassroomListFragmentTest {
*/
@Test
fun testFragment_markStory0DonePlayStory1FirstTestTopic_playFractionsTopic_orderIsCorrect() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic0Story0(
@@ -506,6 +688,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_markStory0DoneFirstTestTopic_suggestedStoriesIsCorrect() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic0Story0(
@@ -526,6 +710,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_markStory0DoneForFractions_recommendedStoriesIsCorrect() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedFractionsStory0(
@@ -554,7 +740,9 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_clickViewAll_opensRecentlyPlayedActivity() {
- logIntoAdminTwice()
+ Intents.init()
+ setUpTestApplicationComponent()
+
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId,
@@ -568,17 +756,23 @@ class ClassroomListFragmentTest {
profileId = profileId,
timestampOlderThanOneWeek = false
)
+ logIntoAdminTwice()
+
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(1)
.assertIsDisplayed()
.performClick()
intended(hasComponent(RecentlyPlayedActivity::class.java.name))
+ Intents.release()
}
@Test
fun testFragment_markFullProgressForFractions_playRatios_displaysRecommendedStories() {
- logIntoAdminTwice()
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0(
profileId = profileId,
@@ -589,6 +783,9 @@ class ClassroomListFragmentTest {
timestampOlderThanOneWeek = false
)
+ logIntoAdminTwice()
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_HEADER_TEST_TAG).onChildAt(0)
.assertTextContains(context.getString(R.string.stories_for_you))
.assertIsDisplayed()
@@ -610,6 +807,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_markAtLeastOneStoryCompletedForAllTopics_displaysComingSoonTopicsList() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedFractionsTopic(
@@ -642,6 +841,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_markFullProgressForSecondTestTopic_displaysComingSoonTopicsText() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic1(
@@ -662,6 +863,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_markStory0OfRatiosAndTestTopics0And1Done_playTestTopicStory0_noPromotions() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedRatiosStory0(
@@ -680,6 +883,7 @@ class ClassroomListFragmentTest {
profileId = profileId,
timestampOlderThanOneWeek = false
)
+ testCoroutineDispatchers.runCurrent()
composeRule.onNodeWithTag(COMING_SOON_TOPIC_LIST_HEADER_TEST_TAG)
.assertTextContains(context.getString(R.string.coming_soon))
@@ -694,6 +898,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_clickPromotedStory_opensTopicActivity() {
+ Intents.init()
+ setUpTestApplicationComponent()
logIntoAdminTwice()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
@@ -701,6 +907,9 @@ class ClassroomListFragmentTest {
timestampOlderThanOneWeek = false
)
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(PROMOTED_STORY_LIST_TEST_TAG).onChildAt(0)
.assertIsDisplayed()
.performClick()
@@ -714,10 +923,16 @@ class ClassroomListFragmentTest {
}.build()
intended(hasComponent(TopicActivity::class.java.name))
intended(hasProtoExtra(TopicActivity.TOPIC_ACTIVITY_PARAMS_KEY, args))
+ Intents.release()
}
@Test
fun testFragment_clickTopicSummary_opensTopicActivityThroughPlayIntent() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(0).performClick()
testCoroutineDispatchers.runCurrent()
@@ -740,12 +955,20 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_scrollToBottom_classroomListSticks_classroomListIsVisible() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(CLASSROOM_LIST_SCREEN_TEST_TAG).performScrollToIndex(3)
composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).assertIsDisplayed()
}
@Test
fun testFragment_scrollToBottom_classroomListCollapsesAndSticks_classroomListIsVisible() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
composeRule.onNodeWithTag(
CLASSROOM_CARD_ICON_TEST_TAG + "_Science",
useUnmergedTree = true
@@ -761,6 +984,10 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_switchClassroom_topicListUpdatesCorrectly() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
// Click on Science classroom card.
composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(0).performClick()
testCoroutineDispatchers.runCurrent()
@@ -792,6 +1019,10 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_clickOnTopicCard_returnBack_classroomSelectionIsRetained() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
+ testCoroutineDispatchers.runCurrent()
+
// Click on Maths classroom card.
composeRule.onNodeWithTag(CLASSROOM_LIST_TEST_TAG).onChildAt(1).performClick()
testCoroutineDispatchers.runCurrent()
@@ -818,6 +1049,8 @@ class ClassroomListFragmentTest {
@Test
fun testFragment_switchClassrooms_topicListUpdatesCorrectly() {
+ setUpTestApplicationComponent()
+ scenario = ActivityScenario.launch(ClassroomListActivity::class.java)
profileTestHelper.logIntoAdmin()
testCoroutineDispatchers.runCurrent()
@@ -870,8 +1103,12 @@ class ClassroomListFragmentTest {
logIntoAdmin()
}
- private fun setUpTestApplicationComponent() {
+ private fun setUpTestApplicationComponent(onboardingV2Enabled: Boolean = false) {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(onboardingV2Enabled)
ApplicationProvider.getApplicationContext().inject(this)
+ testCoroutineDispatchers.registerIdlingResource()
+ profileTestHelper.initializeProfiles()
+ testCoroutineDispatchers.runCurrent()
}
// TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
@@ -911,6 +1148,12 @@ class ClassroomListFragmentTest {
interface Builder : ApplicationComponent.Builder
fun inject(classroomListFragmentTest: ClassroomListFragmentTest)
+
+ fun getAppStartupStateController(): AppStartupStateController
+
+ fun getTestCoroutineDispatchers(): TestCoroutineDispatchers
+
+ fun getProfileTestHelper(): ProfileTestHelper
}
class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
@@ -924,6 +1167,10 @@ class ClassroomListFragmentTest {
component.inject(classroomListFragmentTest)
}
+ public override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ }
+
override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
}
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 46df59bfa2f..f0943148df3 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
@@ -14,6 +14,7 @@ import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.Espresso.pressBackUnconditionally
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
@@ -229,7 +230,6 @@ class HomeActivityTest {
profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build()
profileId1 = ProfileId.newBuilder().setInternalId(internalProfileId1).build()
testCoroutineDispatchers.registerIdlingResource()
- profileTestHelper.initializeProfiles()
}
@After
@@ -263,6 +263,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_loadingItemsSuccess_checkProgressbarIsNotDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
launch(createHomeActivityIntent(internalProfileId)).use {
testCoroutineDispatchers.runCurrent()
@@ -288,6 +289,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_withAdminProfile_configChange_profileNameIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP)
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -304,6 +306,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_morningTimestamp_goodMorningMessageIsDisplayed_withAdminProfileName() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(MORNING_TIMESTAMP)
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -319,6 +322,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_afternoonTimestamp_goodAfternoonMessageIsDisplayed_withAdminProfileName() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(AFTERNOON_TIMESTAMP)
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -334,6 +338,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_eveningTimestamp_goodEveningMessageIsDisplayed_withAdminProfileName() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(EVENING_TIMESTAMP)
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -357,6 +362,7 @@ class HomeActivityTest {
@Test
fun testPromotedStorySpotlight_setToShowOnSecondLogin_notSeenBefore_checkSpotlightShown() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -367,6 +373,7 @@ class HomeActivityTest {
@Test
fun testPromotedStoriesSpotlight_setToShowOnSecondLogin_pressDone_checkSpotlightNotShown() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -391,6 +398,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_recentlyPlayedStoriesTextIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -414,6 +422,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_viewAllTextIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -438,6 +447,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_storiesPlayedOneWeekAgo_displaysLastPlayedStoriesText() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -462,6 +472,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markStory0DoneForFraction_displaysRecommendedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedFractionsTopic(
profileId = profileId1,
@@ -494,6 +505,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markCompletedRatiosStory0_recommendsFractions() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedRatiosStory0(
profileId = profileId1,
@@ -519,6 +531,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_noTopicProgress_initialRecommendationFractionsAndRatiosIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -545,6 +558,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_forPromotedActivityList_hideViewAll() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -565,6 +579,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markStory0DoneForRatiosAndFirstTestTopic_displaysRecommendedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic0Story0(
profileId = profileId1,
@@ -594,6 +609,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markAtLeastOneStoryCompletedForAllTopics_displaysComingSoonTopicsList() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedFractionsTopic(
profileId = profileId1,
@@ -631,6 +647,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markFullProgressForSecondTestTopic_displaysComingSoonTopicsText() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic1(
profileId = profileId1,
@@ -673,6 +690,7 @@ class HomeActivityTest {
*/
@Test
fun testHomeActivity_markStory0DonePlayStory1FirstTestTopic_playFractionsTopic_orderIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic0Story0(
profileId = profileId1,
@@ -718,6 +736,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markStory0OfRatiosAndTestTopics0And1Done_playTestTopicStory0_noPromotions() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedRatiosStory0(
profileId = profileId1,
@@ -755,6 +774,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markStory0DoneFirstTestTopic_recommendedStoriesIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedTestTopic0Story0(
profileId = profileId1,
@@ -780,6 +800,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markStory0DoneForFrac_recommendedStoriesIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedFractionsStory0(
profileId = profileId1,
@@ -811,6 +832,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_clickViewAll_opensRecentlyPlayedActivity() {
+ setUpTestWithOnboardingV2Disabled()
markSpotlightSeen(profileId1)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
@@ -842,6 +864,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_promotedCard_chapterNameIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -861,6 +884,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_promotedCard_storyNameIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -880,6 +904,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_configChange_promotedCard_storyNameIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -904,6 +929,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_markFullProgressForFractions_playRatios_displaysRecommendedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0(
profileId = profileId1,
@@ -939,6 +965,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_clickPromotedStory_opensTopicActivity() {
+ setUpTestWithOnboardingV2Disabled()
markSpotlightSeen(profileId1)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
@@ -970,6 +997,7 @@ class HomeActivityTest {
@Test
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#4700): Make this test work on Espresso.
fun testHomeActivity_promotedStoryHasScalableWidth() {
+ setUpTestWithOnboardingV2Disabled()
fontScaleConfigurationUtil.adjustFontScale(context, ReadingTextSize.EXTRA_LARGE_TEXT_SIZE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
@@ -1002,6 +1030,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_promotedCard_topicNameIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -1025,6 +1054,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_firstTestTopic_topicSummary_opensTopicActivityThroughPlayIntent() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
markSpotlightSeen(profileId1)
launch(createHomeActivityIntent(internalProfileId1)).use {
@@ -1050,6 +1080,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_firstTestTopic_topicSummary_topicNameIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -1064,6 +1095,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_fiveLessons_topicSummary_lessonCountIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -1078,6 +1110,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_secondTestTopic_topicSummary_allTopics_topicNameIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0(
profileId = profileId1,
@@ -1097,6 +1130,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_oneLesson_topicSummary_lessonCountIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -1114,6 +1148,7 @@ class HomeActivityTest {
@Config(qualifiers = "+port-mdpi")
@Test
fun testHomeActivity_longProfileName_welcomeMessageIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(longNameInternalProfileId)).use {
testCoroutineDispatchers.runCurrent()
scrollToPosition(0)
@@ -1132,6 +1167,7 @@ class HomeActivityTest {
@Config(qualifiers = "+land-mdpi")
@Test
fun testHomeActivity_configChange_longProfileName_welcomeMessageIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(longNameInternalProfileId)).use {
onView(isRoot()).perform(orientationLandscape())
testCoroutineDispatchers.runCurrent()
@@ -1151,6 +1187,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-port")
@Test
fun testHomeActivity_longProfileName_tabletPortraitWelcomeMessageIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(longNameInternalProfileId)).use {
testCoroutineDispatchers.runCurrent()
scrollToPosition(0)
@@ -1169,6 +1206,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-land")
@Test
fun testHomeActivity_longProfileName_tabletLandscapeWelcomeMessageIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(longNameInternalProfileId)).use {
onView(isRoot()).perform(orientationLandscape())
testCoroutineDispatchers.runCurrent()
@@ -1185,6 +1223,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_oneLesson_topicSummary_configChange_lessonCountIsCorrect() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -1200,6 +1239,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_clickTopicSummary_opensTopicActivity() {
+ setUpTestWithOnboardingV2Disabled()
logIntoUserTwice()
markSpotlightSeen(profileId1)
launch(createHomeActivityIntent(internalProfileId1)).use {
@@ -1219,6 +1259,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_onBackPressed_exitToProfileChooserDialogIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
pressBack()
@@ -1230,6 +1271,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_onBackPressed_configChange_exitToProfileChooserDialogIsDisplayed() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
@@ -1243,6 +1285,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_onBackPressed_clickExit_opensProfileActivity() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
pressBack()
@@ -1255,6 +1298,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_checkSpanForItem0_spanSizeIsTwoOrThree() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
if (context.resources.getBoolean(R.bool.isTablet)) {
@@ -1267,6 +1311,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_checkSpanForItem4_spanSizeIsOne() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.home_recycler_view)).check(hasGridItemCount(1, 4))
@@ -1275,6 +1320,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_configChange_checkSpanForItem4_spanSizeIsOne() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId1)).use {
testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
@@ -1284,6 +1330,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_allTopicsCompleted_hidesPromotedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markAllTopicsAsCompleted(
profileId = createProfileId(internalProfileId),
@@ -1305,6 +1352,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_partialProgressForFractionsAndRatios_showsRecentlyPlayedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markCompletedFractionsStory0Exp0(
profileId = profileId,
@@ -1332,6 +1380,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_allTopicsCompleted_displaysAllTopicsHeader() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markAllTopicsAsCompleted(
profileId = createProfileId(internalProfileId),
@@ -1352,6 +1401,7 @@ class HomeActivityTest {
@Config(qualifiers = "+port")
@Test
fun testHomeActivity_allTopicsCompleted_mobilePortrait_displaysAllTopicCardsIn2Columns() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markAllTopicsAsCompleted(
profileId = profileId,
@@ -1368,6 +1418,7 @@ class HomeActivityTest {
@Config(qualifiers = "+land")
@Test
fun testHomeActivity_allTopicsCompleted_mobileLandscape_displaysAllTopicCardsIn3Columns() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markAllTopicsAsCompleted(
profileId = profileId,
@@ -1384,6 +1435,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-port")
@Test
fun testHomeActivity_allTopicsCompleted_tabletPortrait_displaysAllTopicCardsIn3Columns() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markAllTopicsAsCompleted(
profileId = profileId,
@@ -1400,6 +1452,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-land")
@Test
fun testHomeActivity_allTopicsCompleted_tabletLandscape_displaysAllTopicCardsIn4Columns() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markAllTopicsAsCompleted(
profileId = profileId,
@@ -1415,6 +1468,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_noTopicsCompleted_displaysAllTopicsHeader() {
+ setUpTestWithOnboardingV2Disabled()
// Only new users will have no progress for any topics.
logIntoAdminTwice()
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -1431,6 +1485,7 @@ class HomeActivityTest {
@Config(qualifiers = "+port")
@Test
fun testHomeActivity_noTopicsStarted_mobilePortraitDisplaysTopicsIn2Columns() {
+ setUpTestWithOnboardingV2Disabled()
// Only new users will have no progress for any topics.
logIntoAdminTwice()
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -1450,6 +1505,7 @@ class HomeActivityTest {
@Config(qualifiers = "+land")
@Test
fun testHomeActivity_noTopicsStarted_mobileLandscapeDisplaysTopicsIn3Columns() {
+ setUpTestWithOnboardingV2Disabled()
// Only new users will have no progress for any topics.
logIntoAdminTwice()
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -1470,6 +1526,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-port")
@Test
fun testHomeActivity_noTopicsStarted_tabletPortraitDisplaysTopicsIn3Columns() {
+ setUpTestWithOnboardingV2Disabled()
// Only new users will have no progress for any topics.
logIntoAdminTwice()
markSpotlightSeen(profileId)
@@ -1486,6 +1543,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-land")
@Test
fun testHomeActivity_noTopicsStarted_tabletLandscapeDisplaysTopicsIn4Columns() {
+ setUpTestWithOnboardingV2Disabled()
// Only new users will have no progress for any topics.
logIntoAdminTwice()
markSpotlightSeen(profileId)
@@ -1502,6 +1560,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_multipleRecentlyPlayedStories_mobileShows3PromotedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedTestTopic0Story0Exp0(
profileId = profileId,
@@ -1539,6 +1598,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-port")
@Test
fun testHomeActivity_multipleRecentlyPlayedStories_tabletPortraitShows3PromotedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedTestTopic0Story0Exp0(
profileId = profileId,
@@ -1577,6 +1637,7 @@ class HomeActivityTest {
@Config(qualifiers = "+sw600dp-land")
@Test
fun testHomeActivity_multipleRecentlyPlayedStories_tabletLandscapeShows4PromotedStories() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
storyProgressTestHelper.markInProgressSavedTestTopic0Story0Exp0(
profileId = profileId,
@@ -1614,6 +1675,7 @@ class HomeActivityTest {
@Test
fun testHomeActivity_onScrollDown_promotedStoryListViewStillShows() {
+ setUpTestWithOnboardingV2Disabled()
// This test is to catch a bug introduced and then fixed in #2246
// (see https://github.com/oppia/oppia-android/pull/2246#pullrequestreview-565964462)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS)
@@ -1642,6 +1704,7 @@ class HomeActivityTest {
@Test
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3840): Make this test work on Espresso.
fun testHomeActivity_defaultState_displaysStringsInEnglish() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(MORNING_TIMESTAMP)
launch(createHomeActivityIntent(internalProfileId)).use {
@@ -1660,6 +1723,7 @@ class HomeActivityTest {
@Test
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3840): Make this test work on Espresso.
fun testHomeActivity_defaultState_hasEnglishAndroidLocale() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId)).use {
testCoroutineDispatchers.runCurrent()
@@ -1673,6 +1737,7 @@ class HomeActivityTest {
@Test
@RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
fun testHomeActivity_defaultState_hasEnglishDisplayLocale() {
+ setUpTestWithOnboardingV2Disabled()
launch(createHomeActivityIntent(internalProfileId)).use {
testCoroutineDispatchers.runCurrent()
@@ -1687,6 +1752,7 @@ class HomeActivityTest {
@Test
@Ignore("Current language switching mechanism doesn't work correctly in Robolectric")
fun testHomeActivity_changeSystemLocaleAndConfigChange_recreatesActivity() {
+ setUpTestWithOnboardingV2Disabled()
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
fakeOppiaClock.setCurrentTimeToSameDateTime(MORNING_TIMESTAMP)
launch(createHomeActivityIntent(internalProfileId)).use { scenario ->
@@ -1730,6 +1796,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3840): Make this test work on Espresso & Robolectric.
fun testHomeActivity_initialArabicContext_displaysStringsInArabic() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(EGYPT_ARABIC_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1755,6 +1822,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3840): Make this test work on Espresso & Robolectric.
fun testHomeActivity_initialArabicContext_isInRtlLayout() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(EGYPT_ARABIC_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1777,6 +1845,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
fun testHomeActivity_initialArabicContext_hasArabicDisplayLocale() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(EGYPT_ARABIC_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1800,6 +1869,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3840): Make this test work on Espresso & Robolectric.
fun testHomeActivity_initialBrazilianPortugueseContext_displayStringsInPortuguese() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1826,6 +1896,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3840): Make this test work on Espresso & Robolectric.
fun testHomeActivity_initialBrazilianPortugueseContext_isInLtrLayout() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1849,6 +1920,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
fun testHomeActivity_initialBrazilianPortugueseContext_hasPortugueseDisplayLocale() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(BRAZIL_PORTUGUESE_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1872,6 +1944,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3840): Make this test work on Espresso & Robolectric.
fun testHomeActivity_initialNigerianPidginContext_isInLtrLayout() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(NIGERIA_NAIJA_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1895,6 +1968,7 @@ class HomeActivityTest {
)
@RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
fun testHomeActivity_initialNigerianPidginContext_hasNaijaDisplayLocale() {
+ setUpTestWithOnboardingV2Disabled()
// Ensure the system locale matches the initial locale context.
forceDefaultLocale(NIGERIA_NAIJA_LOCALE)
fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME)
@@ -1909,6 +1983,91 @@ class HomeActivityTest {
}
}
+ @Test
+ fun testHomeActivity_onBackPressed_soleLearnerProfile_exitsApp() {
+ setUpTestWithOnboardingV2Enabled()
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ markSpotlightSeen(profileId)
+ launch(createHomeActivityIntent(internalProfileId)).use { scenario ->
+ pressBackUnconditionally()
+ // Pressing back should close the activity (and thus, the app) since the Sole learner has
+ // no profile chooser.
+ scenario.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testHomeActivity_onBackPressed_nonSoleLearner_exitToProfileChooserDialogIsDisplayed() {
+ setUpTestWithOnboardingV2Enabled()
+ profileTestHelper.initializeProfiles()
+ markSpotlightSeen(profileId)
+ launch(createHomeActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
+ pressBack()
+ onView(withText(R.string.home_activity_back_dialog_message))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testActivity_onBackPressed_nonSoleLearner_configChange_exitToProfileDialogIsDisplayed() {
+ setUpTestWithOnboardingV2Enabled()
+ profileTestHelper.initializeProfiles()
+ markSpotlightSeen(profileId)
+ launch(createHomeActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
+ pressBack()
+ onView(isRoot()).perform(orientationLandscape())
+ onView(withText(R.string.home_activity_back_dialog_message))
+ .inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testHomeActivity_onBackPressed_clickExitOnDialog_opensProfileActivity() {
+ setUpTestWithOnboardingV2Enabled()
+ profileTestHelper.initializeProfiles()
+ markSpotlightSeen(profileId)
+ launch(createHomeActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
+ pressBack()
+ onView(withText(R.string.home_activity_back_dialog_exit))
+ .inRoot(isDialog())
+ .perform(click())
+ intended(hasComponent(ProfileChooserActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testHomeActivityV1_onBackPressed_clickExitOnDialog_opensProfileActivity() {
+ setUpTestWithOnboardingV2Disabled()
+ profileTestHelper.initializeProfiles()
+ markSpotlightSeen(profileId)
+ launch(createHomeActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
+ pressBack()
+ onView(withText(R.string.home_activity_back_dialog_exit))
+ .inRoot(isDialog())
+ .perform(click())
+ intended(hasComponent(ProfileChooserActivity::class.java.name))
+ }
+ }
+
+ private fun setUpTestWithOnboardingV2Enabled() {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(true)
+ setUpTestApplicationComponent()
+ }
+
+ private fun setUpTestWithOnboardingV2Disabled() {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(true)
+ setUpTestApplicationComponent()
+ profileTestHelper.initializeProfiles()
+ }
+
private fun markSpotlightSeen(profileId: ProfileId) {
spotlightStateController.markSpotlightViewed(profileId, Spotlight.FeatureCase.PROMOTED_STORIES)
testCoroutineDispatchers.runCurrent()
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
index 0db62fa800b..fdc6ba55566 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
@@ -37,6 +37,7 @@ 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.model.ProfileId
import org.oppia.android.app.options.AudioLanguageActivity
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
@@ -72,11 +73,14 @@ 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.FakeAnalyticsEventLogger
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.logging.EventLogSubject.Companion.assertThat
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
import org.oppia.android.testing.threading.TestDispatcherModule
@@ -95,6 +99,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
@@ -115,13 +120,18 @@ class IntroFragmentTest {
@get:Rule val oppiaTestRule = OppiaTestRule()
@Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
@Inject lateinit var context: Context
+ @Inject lateinit var profileTestHelper: ProfileTestHelper
+ @Inject lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger
private val testProfileNickname = "John"
+ private val testInternalProfileId = 0
+ private val testProfileId = ProfileId.newBuilder().setInternalId(testInternalProfileId).build()
@Before
fun setUp() {
Intents.init()
setUpTestApplicationComponent()
+ profileTestHelper.initializeProfiles()
testCoroutineDispatchers.registerIdlingResource()
}
@@ -209,6 +219,16 @@ class IntroFragmentTest {
}
}
+ @Test
+ fun testFragment_launchFragment_logsProfileOnboardingStartedEvent() {
+ launchOnboardingLearnerIntroActivity().use {
+ val event = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(event).hasStartProfileOnboardingContextThat {
+ hasProfileIdThat().isEqualTo(testProfileId)
+ }
+ }
+ }
+
private fun launchOnboardingLearnerIntroActivity():
ActivityScenario? {
val params = IntroActivityParams.newBuilder()
@@ -218,6 +238,7 @@ class IntroFragmentTest {
val scenario = ActivityScenario.launch(
IntroActivity.createIntroActivity(context).apply {
putProtoExtra(IntroActivity.PARAMS_KEY, params)
+ decorateWithUserProfileId(testProfileId)
}
)
testCoroutineDispatchers.runCurrent()
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 8493d3ae7ed..1077e1a3afc 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
@@ -39,6 +39,7 @@ 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.CreateProfileActivityParams
+import org.oppia.android.app.model.ProfileId
import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.profile.ProfileChooserActivity
@@ -76,11 +77,14 @@ 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.FakeAnalyticsEventLogger
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.logging.EventLogSubject
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
import org.oppia.android.testing.threading.TestDispatcherModule
@@ -99,6 +103,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.decorateWithUserProfileId
import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
@@ -121,19 +126,19 @@ class OnboardingProfileTypeFragmentTest {
@get:Rule
val oppiaTestRule = OppiaTestRule()
- @Inject
- lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject lateinit var context: Context
+ @Inject lateinit var machineLocale: OppiaLocale.MachineLocale
+ @Inject lateinit var profileTestHelper: ProfileTestHelper
+ @Inject lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger
- @Inject
- lateinit var context: Context
-
- @Inject
- lateinit var machineLocale: OppiaLocale.MachineLocale
+ private val testProfileId = ProfileId.newBuilder().setInternalId(0).build()
@Before
fun setUp() {
Intents.init()
setUpTestApplicationComponent()
+ profileTestHelper.initializeProfiles()
testCoroutineDispatchers.registerIdlingResource()
}
@@ -335,10 +340,24 @@ class OnboardingProfileTypeFragmentTest {
}
}
+ @Test
+ fun testFragment_launchFragment_logsProfileOnboardingStartedEvent() {
+ launchOnboardingProfileTypeActivity().use {
+ onView(withId(R.id.profile_type_supervisor_navigation_card)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ val event = fakeAnalyticsEventLogger.getMostRecentEvent()
+ EventLogSubject.assertThat(event).hasStartProfileOnboardingContextThat {
+ hasProfileIdThat().isEqualTo(testProfileId)
+ }
+ }
+ }
+
private fun launchOnboardingProfileTypeActivity():
ActivityScenario? {
val scenario = ActivityScenario.launch(
- OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(context)
+ OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(context).apply {
+ decorateWithUserProfileId(testProfileId)
+ }
)
testCoroutineDispatchers.runCurrent()
return scenario
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 cb3fef2835f..703b2c7cf94 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
@@ -42,6 +42,7 @@ import org.oppia.android.app.application.ApplicationInjectorProvider
import org.oppia.android.app.application.ApplicationModule
import org.oppia.android.app.application.ApplicationStartupListenerModule
import org.oppia.android.app.application.testing.TestingBuildFlavorModule
+import org.oppia.android.app.classroom.ClassroomListActivity
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.home.HomeActivity
@@ -319,6 +320,7 @@ class AudioLanguageFragmentTest {
@Test
fun testFragment_portraitMode_continueButtonClicked_launchesHomeScreen() {
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(false)
initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
launch(
createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
@@ -335,6 +337,7 @@ class AudioLanguageFragmentTest {
@Test
fun testFragment_landscapeMode_continueButtonClicked_launchesHomeScreen() {
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(false)
initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
launch(
createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
@@ -349,9 +352,44 @@ class AudioLanguageFragmentTest {
}
}
+ @Test
+ fun testFragment_multipleClassroomsEnabled_continueButtonClicked_launchesClassroomScreen() {
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(true)
+ initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
+ launch(
+ createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
+ ).use {
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // Verifies that accepting the default language selection works correctly.
+ intended(hasComponent(ClassroomListActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_multipleClassroomsEnabled_continueButtonLaunchesClassroomScreen() {
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(true)
+ 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()
+
+ // Verifies that accepting the default language selection works correctly.
+ intended(hasComponent(ClassroomListActivity::class.java.name))
+ }
+ }
+
@Test
@RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
fun testFragment_languageSelectionChanged_selectionIsUpdated() {
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(false)
initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
launch(
createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
@@ -381,6 +419,7 @@ class AudioLanguageFragmentTest {
@Test
@RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL])
fun testFragment_languageSelectionChanged_configChange_selectionIsUpdated() {
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(false)
initializeTestApplicationComponent(enableOnboardingFlowV2 = true)
launch(
createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE)
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 07d94b978f7..f7abd23fcc6 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
@@ -21,7 +21,6 @@ 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 androidx.test.rule.ActivityTestRule
import dagger.Component
import org.hamcrest.Matchers.allOf
import org.junit.After
@@ -156,13 +155,6 @@ class OptionsFragmentTest {
ApplicationProvider.getApplicationContext().inject(this)
}
- @get:Rule
- var optionActivityTestRule: ActivityTestRule = ActivityTestRule(
- OptionsActivity::class.java,
- /* initialTouchMode= */ true,
- /* launchActivity= */ false
- )
-
private fun createOptionActivityIntent(
internalProfileId: Int,
isFromNavigationDrawer: Boolean
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 15479e71e4f..c851c309879 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
@@ -45,9 +45,11 @@ import org.oppia.android.app.classroom.ClassroomListActivity
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
import org.oppia.android.app.home.HomeActivity
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
+import org.oppia.android.app.onboarding.IntroActivity
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.profile.AdminAuthActivity.Companion.ADMIN_AUTH_ACTIVITY_PARAMS_KEY
-import org.oppia.android.app.profile.AdminPinActivity.Companion.ADMIN_PIN_ACTIVITY_PARAMS_KEY
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
import org.oppia.android.app.shim.ViewBindingShimModule
@@ -156,6 +158,7 @@ class ProfileChooserFragmentTest {
@After
fun tearDown() {
testCoroutineDispatchers.unregisterIdlingResource()
+ TestPlatformParameterModule.reset()
Intents.release()
}
@@ -325,7 +328,8 @@ class ProfileChooserFragmentTest {
}
@Test
- fun testProfileChooserFragment_clickProfile_checkOpensPinPasswordActivity() {
+ fun testProfileChooserFragment_onboardingV1_clickAdminProfile_checkOpensPinPasswordActivity() {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(false)
profileTestHelper.initializeProfiles(autoLogIn = false)
launch(ProfileChooserActivity::class.java).use {
testCoroutineDispatchers.runCurrent()
@@ -340,26 +344,83 @@ class ProfileChooserFragmentTest {
}
@Test
- fun testProfileChooserFragment_clickAdminProfileWithNoPin_checkOpensAdminPinActivity() {
+ fun testMigrateProfiles_onboardingV2_clickAdminProfile_checkOpensPinPasswordActivity() {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(true)
+ profileTestHelper.initializeProfiles(autoLogIn = true)
+ val adminProfileId = ProfileId.newBuilder().setInternalId(0).build()
+ profileTestHelper.updateProfileType(
+ profileId = adminProfileId,
+ profileType = ProfileType.SUPERVISOR
+ )
+
+ launch(ProfileChooserActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+ onView(
+ atPosition(
+ recyclerViewId = R.id.profile_recycler_view,
+ position = 0
+ )
+ ).perform(click())
+ intended(hasComponent(PinPasswordActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testMigrateProfiles_onboardingV2_clickLearnerWithPin_checkOpensIntroActivity() {
+ profileTestHelper.initializeProfiles(autoLogIn = true)
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(true)
+
+ launch(ProfileChooserActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+ onView(
+ atPosition(
+ recyclerViewId = R.id.profile_recycler_view,
+ position = 1
+ )
+ ).perform(click())
+ intended(hasComponent(IntroActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testMigrateProfiles_onboardingV2_clickAdminWithoutPin_checkOpensIntroActivity() {
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(true)
+
+ launch(ProfileChooserActivity::class.java).use {
+ testCoroutineDispatchers.runCurrent()
+ onView(
+ atPosition(
+ recyclerViewId = R.id.profile_recycler_view,
+ position = 0
+ )
+ ).perform(click())
+ intended(hasComponent(IntroActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testMigrateProfiles_onboardingV2_clickLearnerWithoutPin_checkOpensIntroActivity() {
+ profileTestHelper.addOnlyAdminProfile()
profileManagementController.addProfile(
- name = "Admin",
+ name = "Learner",
pin = "",
avatarImagePath = null,
allowDownloadAccess = true,
colorRgb = -10710042,
- isAdmin = true
+ isAdmin = false
)
- launch(createProfileChooserActivityIntent()).use {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(true)
+
+ launch(ProfileChooserActivity::class.java).use {
testCoroutineDispatchers.runCurrent()
onView(
- atPositionOnView(
+ atPosition(
recyclerViewId = R.id.profile_recycler_view,
- position = 1,
- targetViewId = R.id.add_profile_item
+ position = 1
)
).perform(click())
- intended(hasComponent(AdminPinActivity::class.java.name))
- intended(hasExtraWithKey(ADMIN_PIN_ACTIVITY_PARAMS_KEY))
+ intended(hasComponent(IntroActivity::class.java.name))
}
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/splash/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/splash/BUILD.bazel
index be9324b4937..304619542b8 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/splash/BUILD.bazel
+++ b/app/src/sharedTest/java/org/oppia/android/app/splash/BUILD.bazel
@@ -29,6 +29,7 @@ app_test(
"//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner",
"//testing/src/main/java/org/oppia/android/testing/junit:parameterized_auto_android_test_runner",
"//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner",
+ "//testing/src/main/java/org/oppia/android/testing/platformparameter:test_module",
"//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
"//testing/src/main/java/org/oppia/android/testing/threading:coroutine_executor_service",
"//testing/src/main/java/org/oppia/android/testing/threading:test_module",
diff --git a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt
index 0cc3ccec366..12e51153159 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt
@@ -41,9 +41,12 @@ 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.classroom.ClassroomListActivity
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.home.HomeActivity
import org.oppia.android.app.model.BuildFlavor
+import org.oppia.android.app.model.IntroActivityParams
import org.oppia.android.app.model.OppiaLanguage.ARABIC
import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE
import org.oppia.android.app.model.OppiaLanguage.ENGLISH
@@ -51,13 +54,17 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED
import org.oppia.android.app.model.OppiaLanguage.NIGERIAN_PIDGIN
import org.oppia.android.app.model.OppiaLocaleContext
import org.oppia.android.app.model.OppiaRegion
+import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.ScreenName
+import org.oppia.android.app.onboarding.IntroActivity
import org.oppia.android.app.onboarding.OnboardingActivity
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.AppLanguageLocaleHandler
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
@@ -87,7 +94,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
@@ -103,6 +109,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration
import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter
import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform
import org.oppia.android.testing.junit.ParameterizedAutoAndroidTestRunner
+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
import org.oppia.android.testing.threading.TestDispatcherModule
@@ -121,6 +129,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
import org.oppia.android.util.parser.image.GlideImageLoaderModule
import org.oppia.android.util.parser.image.ImageParsingModule
+import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import java.io.File
@@ -159,6 +168,8 @@ class SplashActivityTest {
lateinit var monitorFactory: DataProviderTestMonitor.Factory
@Inject
lateinit var appStartupStateController: AppStartupStateController
+ @Inject
+ lateinit var profileTestHelper: ProfileTestHelper
@Parameter
lateinit var firstOpen: String
@@ -177,6 +188,7 @@ class SplashActivityTest {
@After
fun tearDown() {
+ TestPlatformParameterModule.reset()
testCoroutineDispatchers.unregisterIdlingResource()
Intents.release()
}
@@ -946,7 +958,6 @@ class SplashActivityTest {
}
@Test
- @RunOn(TestPlatform.ROBOLECTRIC)
fun testSplashActivity_onboarded_devFlavor_doesNotWaitToStart() {
simulateAppAlreadyOnboardedWithFlavor(BuildFlavor.DEVELOPER)
initializeTestApplicationWithFlavor(BuildFlavor.DEVELOPER)
@@ -1049,6 +1060,108 @@ class SplashActivityTest {
}
}
+ @Test
+ fun testSplashActivity_initialOpen_onboardingV2Enabled_routesToOnboardingActivity() {
+ initializeTestApplication(onboardingV2Enabled = true)
+
+ launchSplashActivityPartially {
+ intended(hasComponent(OnboardingActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testSplashActivity_onboardingV2Enabled_profilePartiallyOnboarded_routesToIntroActivity() {
+ initializeTestApplication(onboardingV2Enabled = true)
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ val profileId = ProfileId.newBuilder().setInternalId(0).build()
+ profileTestHelper.updateProfileType(profileId, ProfileType.SOLE_LEARNER)
+ profileTestHelper.markProfileOnboardingStarted(profileId)
+ val params = IntroActivityParams.newBuilder()
+ .setProfileNickname("Admin")
+ .build()
+
+ launchSplashActivityPartially {
+ intended(hasComponent(IntroActivity::class.java.name))
+ intended(hasProtoExtra(IntroActivity.PARAMS_KEY, params))
+ intended(hasProtoExtra(PROFILE_ID_INTENT_DECORATOR, profileId))
+ }
+ }
+
+ @Test
+ fun testSplashActivity_onboardingV2Enabled_onboardedSoleLearnerProfile_routesToHomeActivity() {
+ simulateAppAlreadyOnboarded()
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(false)
+ initializeTestApplication(onboardingV2Enabled = true)
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ testCoroutineDispatchers.runCurrent()
+
+ val profileId = ProfileId.newBuilder().setInternalId(0).build()
+ monitorFactory.waitForNextSuccessfulResult(
+ profileTestHelper.updateProfileType(profileId, ProfileType.SOLE_LEARNER)
+ )
+
+ monitorFactory.waitForNextSuccessfulResult(
+ profileTestHelper.markProfileOnboardingStarted(profileId)
+ )
+ monitorFactory.waitForNextSuccessfulResult(
+ profileTestHelper.markProfileOnboardingEnded(profileId)
+ )
+ testCoroutineDispatchers.runCurrent()
+
+ launchSplashActivityPartially {
+ intended(hasComponent(HomeActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testSplashActivity_onboardingV2_onboardedSoleLearnerProfile_routesToClassroomListActivity() {
+ simulateAppAlreadyOnboarded()
+ TestPlatformParameterModule.forceEnableMultipleClassrooms(true)
+ initializeTestApplication(onboardingV2Enabled = true)
+ testCoroutineDispatchers.unregisterIdlingResource()
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ testCoroutineDispatchers.runCurrent()
+
+ val profileId = ProfileId.newBuilder().setInternalId(0).build()
+ monitorFactory.waitForNextSuccessfulResult(
+ profileTestHelper.updateProfileType(profileId, ProfileType.SOLE_LEARNER)
+ )
+
+ monitorFactory.waitForNextSuccessfulResult(
+ profileTestHelper.markProfileOnboardingStarted(profileId)
+ )
+ monitorFactory.waitForNextSuccessfulResult(
+ profileTestHelper.markProfileOnboardingEnded(profileId)
+ )
+ testCoroutineDispatchers.runCurrent()
+
+ launchSplashActivityPartially {
+ intended(hasComponent(ClassroomListActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testSplashActivity_onboardingV2_onboardedAdminProfile_routesToProfileChooserActivity() {
+ simulateAppAlreadyOnboarded()
+ initializeTestApplication(onboardingV2Enabled = true)
+ profileTestHelper.addOnlyAdminProfile()
+
+ launchSplashActivityPartially {
+ intended(hasComponent(ProfileChooserActivity::class.java.name))
+ }
+ }
+
+ @Test
+ fun testActivity_onboardingV2Enabled_existingMultipleProfiles_routesToProfileChooserActivity() {
+ simulateAppAlreadyOnboarded()
+ initializeTestApplication(onboardingV2Enabled = true)
+ profileTestHelper.addMoreProfiles(5)
+
+ launchSplashActivityPartially {
+ intended(hasComponent(ProfileChooserActivity::class.java.name))
+ }
+ }
+
private fun simulateAppAlreadyOnboarded() {
// Simulate the app was already onboarded by creating an isolated onboarding flow controller and
// saving the onboarding status on the system before the activity is opened. Note that this has
@@ -1114,8 +1227,9 @@ class SplashActivityTest {
simulateAppAlreadyOnboarded()
}
- private fun initializeTestApplication() {
+ private fun initializeTestApplication(onboardingV2Enabled: Boolean = false) {
ApplicationProvider.getApplicationContext().inject(this)
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(onboardingV2Enabled)
testCoroutineDispatchers.registerIdlingResource()
setAutoAppExpirationEnabled(enabled = false) // Default to disabled.
}
@@ -1203,7 +1317,7 @@ class SplashActivityTest {
@Component(
modules = [
TestModule::class, RobolectricModule::class,
- TestDispatcherModule::class, ApplicationModule::class, PlatformParameterModule::class,
+ TestDispatcherModule::class, ApplicationModule::class, TestPlatformParameterModule::class,
LoggerModule::class, ContinueModule::class, FractionInputModule::class,
ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
@@ -1245,6 +1359,8 @@ class SplashActivityTest {
fun getMonitorFactory(): DataProviderTestMonitor.Factory
+ fun getProfieTestHelper(): ProfileTestHelper
+
fun inject(splashActivityTest: SplashActivityTest)
}
@@ -1257,6 +1373,8 @@ class SplashActivityTest {
get() = component.getTestCoroutineDispatchers()
val monitorFactory: DataProviderTestMonitor.Factory
get() = component.getMonitorFactory()
+ val profileTestHelper: ProfileTestHelper
+ get() = component.getProfieTestHelper()
fun inject(splashActivityTest: SplashActivityTest) {
component.inject(splashActivityTest)
diff --git a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt
index 9930513107a..54f277718e8 100644
--- a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt
@@ -27,8 +27,11 @@ 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.EventLog
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.COMPLETE_APP_ONBOARDING
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.END_PROFILE_ONBOARDING_EVENT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_HOME
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.IntentFactoryShimModule
import org.oppia.android.app.shim.ViewBindingShimModule
@@ -61,7 +64,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
@@ -70,6 +72,8 @@ import org.oppia.android.testing.TestLogReportingModule
import org.oppia.android.testing.data.DataProviderTestMonitor
import org.oppia.android.testing.firebase.TestAuthenticationModule
import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
+import org.oppia.android.testing.platformparameter.TestPlatformParameterModule
+import org.oppia.android.testing.profile.ProfileTestHelper
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestCoroutineDispatchers
import org.oppia.android.testing.threading.TestDispatcherModule
@@ -114,7 +118,12 @@ class HomeActivityLocalTest {
@Inject
lateinit var monitorFactory: DataProviderTestMonitor.Factory
- private val profileId: ProfileId = ProfileId.newBuilder().setInternalId(1).build()
+ @Inject
+ lateinit var profileTestHelper: ProfileTestHelper
+
+ private val internalProfileId: Int = 0
+
+ private val profileId: ProfileId = ProfileId.newBuilder().setInternalId(internalProfileId).build()
@Before
fun setUp() {
@@ -123,12 +132,13 @@ class HomeActivityLocalTest {
@After
fun tearDown() {
+ TestPlatformParameterModule.reset()
Intents.release()
}
@Test
- fun testHomeActivity_onLaunch_logsEvent() {
- setUpTestApplicationComponent()
+ fun testHomeActivity_onLaunch_logsOpenHomeEvent() {
+ setUpTestWithOnboardingV2Enabled(false)
launch(createHomeActivityIntent(profileId)).use {
testCoroutineDispatchers.runCurrent()
@@ -140,13 +150,75 @@ class HomeActivityLocalTest {
}
@Test
- fun testHomeActivity_onSubsequentLaunch_doesNotLogCompletedOnboardingEvent() {
+ fun testActivity_onboardingV2_soleProfile_onInitialLaunch_logsCompleteAppOnboardingEvent() {
+ setUpTestWithOnboardingV2Enabled(true)
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId,
+ profileType = ProfileType.SOLE_LEARNER
+ )
+ launch(createHomeActivityIntent(profileId)).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val hasCompleteAppOnboardingEvent = fakeAnalyticsEventLogger.hasEventLogged {
+ it.context.activityContextCase == COMPLETE_APP_ONBOARDING
+ }
+ assertThat(hasCompleteAppOnboardingEvent).isTrue()
+ }
+ }
+
+ @Test
+ fun testActivity_onboardingV2_supervisorProfile_onInitialLaunch_logsCompleteAppOnboardingEvent() {
+ setUpTestWithOnboardingV2Enabled(true)
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ profileTestHelper.updateProfileType(
+ profileId = profileId,
+ profileType = ProfileType.SUPERVISOR
+ )
+ launch(createHomeActivityIntent(profileId)).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val hasCompleteAppOnboardingEvent = fakeAnalyticsEventLogger.hasEventLogged {
+ it.context.activityContextCase == COMPLETE_APP_ONBOARDING
+ }
+ assertThat(hasCompleteAppOnboardingEvent).isTrue()
+ }
+ }
+
+ @Test
+ fun testActivity_onboardingV2_nonAdminProfile_onInitialLaunch_doesNotLogAppOnboardingEvent() {
+ setUpTestWithOnboardingV2Enabled(true)
+ profileTestHelper.addOnlyAdminProfile()
+ profileTestHelper.addMoreProfiles(1)
+ val profileId1 = ProfileId.newBuilder().setInternalId(1).build()
+ profileTestHelper.updateProfileType(
+ profileId = profileId1,
+ profileType = ProfileType.ADDITIONAL_LEARNER
+ )
+ launch(createHomeActivityIntent(profileId1)).use {
+ testCoroutineDispatchers.runCurrent()
+ val events = fakeAnalyticsEventLogger.getMostRecentEvents(2)
+ val eventCount = fakeAnalyticsEventLogger.getEventListCount()
+
+ assertThat(eventCount).isEqualTo(2)
+ assertThat(events.first().priority).isEqualTo(EventLog.Priority.ESSENTIAL)
+ assertThat(events.first().context.activityContextCase).isEqualTo(OPEN_HOME)
+ assertThat(events.last().priority).isEqualTo(EventLog.Priority.OPTIONAL)
+ assertThat(events.last().context.activityContextCase).isEqualTo(END_PROFILE_ONBOARDING_EVENT)
+ }
+ }
+
+ @Test
+ fun testActivity_onboardingV2_adminProfile_onSubsequentLaunch_doesNotLogAppOnboardingEvent() {
executeInPreviousAppInstance { testComponent ->
+ testComponent.getProfileTestHelper().updateProfileType(profileId, ProfileType.SOLE_LEARNER)
+ testComponent.getProfileTestHelper().markProfileOnboardingStarted(profileId)
+ testComponent.getProfileTestHelper().markProfileOnboardingEnded(profileId)
testComponent.getAppStartupStateController().markOnboardingFlowCompleted()
testComponent.getTestCoroutineDispatchers().runCurrent()
}
- setUpTestApplicationComponent()
+ setUpTestWithOnboardingV2Enabled(false)
launch(createHomeActivityIntent(profileId)).use {
testCoroutineDispatchers.runCurrent()
val eventCount = fakeAnalyticsEventLogger.getEventListCount()
@@ -158,6 +230,61 @@ class HomeActivityLocalTest {
}
}
+ @Test
+ fun testHomeActivity_onSubsequentLaunch_doesNotLogCompletedAppOnboardingEvent() {
+ executeInPreviousAppInstance { testComponent ->
+ testComponent.getAppStartupStateController().markOnboardingFlowCompleted()
+ testComponent.getTestCoroutineDispatchers().runCurrent()
+ }
+
+ setUpTestWithOnboardingV2Enabled(false)
+ launch(createHomeActivityIntent(profileId)).use {
+ testCoroutineDispatchers.runCurrent()
+ val eventCount = fakeAnalyticsEventLogger.getEventListCount()
+ val event = fakeAnalyticsEventLogger.getMostRecentEvent()
+
+ assertThat(eventCount).isEqualTo(1)
+ assertThat(event.priority).isEqualTo(EventLog.Priority.ESSENTIAL)
+ assertThat(event.context.activityContextCase).isEqualTo(OPEN_HOME)
+ }
+ }
+
+ @Test
+ fun testHomeActivity_onboardingV2Enabled_onInitialLaunch_logsEndProfileOnboardingEvent() {
+ setUpTestWithOnboardingV2Enabled(true)
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ launch(createHomeActivityIntent(profileId)).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val hasProfileOnboardingEndedEvent = fakeAnalyticsEventLogger.hasEventLogged {
+ it.context.activityContextCase == END_PROFILE_ONBOARDING_EVENT
+ }
+ assertThat(hasProfileOnboardingEndedEvent).isTrue()
+ }
+ }
+
+ @Test
+ fun testHomeActivity_onboardingV2_revisitApp_doesNotLogEndProfileOnboardingEvent() {
+ executeInPreviousAppInstance { testComponent ->
+ testComponent.getAppStartupStateController().markOnboardingFlowCompleted()
+ testComponent.getProfileTestHelper().markProfileOnboardingEnded(profileId)
+ testComponent.getTestCoroutineDispatchers().runCurrent()
+ }
+
+ setUpTestWithOnboardingV2Enabled(true)
+ launch(createHomeActivityIntent(profileId)).use {
+ testCoroutineDispatchers.runCurrent()
+
+ val event = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(event.context.activityContextCase).isEqualTo(OPEN_HOME)
+ }
+ }
+
+ private fun setUpTestWithOnboardingV2Enabled(enableOnboardingFlowV2: Boolean) {
+ TestPlatformParameterModule.forceEnableOnboardingFlowV2(enableOnboardingFlowV2)
+ setUpTestApplicationComponent()
+ }
+
/**
* Creates a separate test application component and executes the specified block. This should be
* called before [setUpTestApplicationComponent] to avoid undefined behavior in production code.
@@ -192,7 +319,7 @@ class HomeActivityLocalTest {
@Component(
modules = [
TestDispatcherModule::class, ApplicationModule::class, RobolectricModule::class,
- PlatformParameterModule::class, PlatformParameterSingletonModule::class,
+ TestPlatformParameterModule::class, PlatformParameterSingletonModule::class,
LoggerModule::class, ContinueModule::class, FractionInputModule::class,
ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
@@ -230,6 +357,8 @@ class HomeActivityLocalTest {
fun getAppStartupStateController(): AppStartupStateController
fun getTestCoroutineDispatchers(): TestCoroutineDispatchers
+
+ fun getProfileTestHelper(): ProfileTestHelper
}
class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
diff --git a/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt b/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt
index 43e959982c6..bc435ce1256 100644
--- a/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt
@@ -137,15 +137,15 @@ class AppStartupStateController @Inject constructor(
): StartupMode {
// Process and return either a StartupMode.APP_IS_DEPRECATED, StartupMode.USER_IS_ONBOARDED or
// StartupMode.USER_NOT_YET_ONBOARDED if the app and OS deprecation feature flag is not enabled.
- if (!enableAppAndOsDeprecation.get().value) {
+ return if (!enableAppAndOsDeprecation.get().value) {
return when {
hasAppExpired() -> StartupMode.APP_IS_DEPRECATED
onboardingState.alreadyOnboardedApp -> StartupMode.USER_IS_ONBOARDED
else -> StartupMode.USER_NOT_YET_ONBOARDED
}
+ } else {
+ deprecationController.processStartUpMode(onboardingState, deprecationResponseDatabase)
}
-
- return deprecationController.processStartUpMode(onboardingState, deprecationResponseDatabase)
}
private fun computeBuildNoticeMode(
diff --git a/domain/src/main/java/org/oppia/android/domain/onboarding/DeprecationController.kt b/domain/src/main/java/org/oppia/android/domain/onboarding/DeprecationController.kt
index 37afcfd0b51..0c000b8dec5 100644
--- a/domain/src/main/java/org/oppia/android/domain/onboarding/DeprecationController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/onboarding/DeprecationController.kt
@@ -160,20 +160,18 @@ class DeprecationController @Inject constructor(
val forcedAppDeprecationDialogHasNotBeenShown =
previousDeprecatedAppVersion < forcedAppUpdateVersionCode.get().value
- if (onboardingState.alreadyOnboardedApp) {
- if (osIsDeprecated && osDeprecationDialogHasNotBeenShown) {
- return StartupMode.OS_IS_DEPRECATED
+ return if (onboardingState.alreadyOnboardedApp) {
+ when {
+ osIsDeprecated && osDeprecationDialogHasNotBeenShown -> StartupMode.OS_IS_DEPRECATED
+ forcedAppUpdateIsAvailable && forcedAppDeprecationDialogHasNotBeenShown ->
+ StartupMode.APP_IS_DEPRECATED
+ optionalAppUpdateIsAvailable && optionalAppDeprecationDialogHasNotBeenShown -> {
+ StartupMode.OPTIONAL_UPDATE_AVAILABLE
+ }
+ else -> StartupMode.USER_IS_ONBOARDED
}
-
- if (forcedAppUpdateIsAvailable && forcedAppDeprecationDialogHasNotBeenShown) {
- return StartupMode.APP_IS_DEPRECATED
- }
-
- if (optionalAppUpdateIsAvailable && optionalAppDeprecationDialogHasNotBeenShown) {
- return StartupMode.OPTIONAL_UPDATE_AVAILABLE
- }
-
- return StartupMode.USER_IS_ONBOARDED
- } else return StartupMode.USER_NOT_YET_ONBOARDED
+ } else {
+ StartupMode.USER_NOT_YET_ONBOARDED
+ }
}
}
diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/OppiaLogger.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/OppiaLogger.kt
index 7a791ebc7cc..a81532a2403 100644
--- a/domain/src/main/java/org/oppia/android/domain/oppialogger/OppiaLogger.kt
+++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/OppiaLogger.kt
@@ -2,6 +2,7 @@ package org.oppia.android.domain.oppialogger
import org.oppia.android.app.model.EventLog
import org.oppia.android.app.model.EventLog.RevisionCardContext
+import org.oppia.android.app.model.ProfileId
import org.oppia.android.util.logging.ConsoleLogger
import javax.inject.Inject
@@ -219,9 +220,7 @@ class OppiaLogger @Inject constructor(private val consoleLogger: ConsoleLogger)
}.build()
}
- /**
- * Returns the context of the event indicating that the user saw the survey popup dialog.
- */
+ /** Returns the context of the event indicating that the user saw the survey popup dialog. */
fun createShowSurveyPopupContext(
explorationId: String,
topicId: String,
@@ -236,9 +235,7 @@ class OppiaLogger @Inject constructor(private val consoleLogger: ConsoleLogger)
.build()
}
- /**
- * Returns the context of the event indicating that the user began a survey session.
- */
+ /** Returns the context of the event indicating that the user began a survey session. */
fun createBeginSurveyContext(
explorationId: String,
topicId: String,
@@ -265,6 +262,24 @@ class OppiaLogger @Inject constructor(private val consoleLogger: ConsoleLogger)
).build()
}
+ /** Returns the context of the event indicating that a profile started onboarding. */
+ fun createProfileOnboardingStartedContext(profileId: ProfileId): EventLog.Context {
+ return EventLog.Context.newBuilder().setStartProfileOnboardingEvent(
+ EventLog.ProfileOnboardingContext.newBuilder()
+ .setProfileId(profileId)
+ .build()
+ ).build()
+ }
+
+ /** Returns the context of the event indicating that a profile completed onboarding. */
+ fun createProfileOnboardingEndedContext(profileId: ProfileId): EventLog.Context {
+ return EventLog.Context.newBuilder().setEndProfileOnboardingEvent(
+ EventLog.ProfileOnboardingContext.newBuilder()
+ .setProfileId(profileId)
+ .build()
+ ).build()
+ }
+
/**
* Returns the context of the event indicating that a console error was logged.
*/
diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsController.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsController.kt
index 76bac6fe92f..6acae963105 100644
--- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsController.kt
@@ -323,9 +323,7 @@ class AnalyticsController @Inject constructor(
}
}
- /**
- * Listens to the flow emitted by the [ConsoleLogger] and logs the error messages.
- */
+ /** Listens to the flow emitted by the [ConsoleLogger] and logs the error messages. */
fun listenForConsoleErrorLogs() {
CoroutineScope(backgroundDispatcher).launch {
consoleLogger.logErrorMessagesFlow.collect { consoleLoggerContext ->
@@ -382,9 +380,7 @@ class AnalyticsController @Inject constructor(
}
}
- /**
- * Logs an [EventLog.CompleteAppOnboardingContext] event with the given [ProfileId].
- */
+ /** Logs an [EventLog.CompleteAppOnboardingContext] event with the given [ProfileId]. */
fun logAppOnboardedEvent(profileId: ProfileId?) {
logLowPriorityEvent(
oppiaLogger.createAppOnBoardingContext(),
@@ -392,6 +388,22 @@ class AnalyticsController @Inject constructor(
)
}
+ /** Logs an [EventLog.ProfileOnboardingContext] event with the given [ProfileId]. */
+ fun logProfileOnboardingStartedContext(profileId: ProfileId) {
+ logLowPriorityEvent(
+ oppiaLogger.createProfileOnboardingStartedContext(profileId),
+ profileId = profileId
+ )
+ }
+
+ /** Logs an [EventLog.ProfileOnboardingContext] event with the given [ProfileId]. */
+ fun logProfileOnboardingEndedContext(profileId: ProfileId) {
+ logLowPriorityEvent(
+ oppiaLogger.createProfileOnboardingEndedContext(profileId),
+ profileId = profileId
+ )
+ }
+
private companion object {
private suspend fun resolveProfileOperation(
profileId: ProfileId?,
diff --git a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt
index 95438d0b9d0..8ba807cdbf5 100644
--- a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt
@@ -16,6 +16,7 @@ import org.oppia.android.app.model.Profile
import org.oppia.android.app.model.ProfileAvatar
import org.oppia.android.app.model.ProfileDatabase
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileOnboardingMode
import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.ReadingTextSize
import org.oppia.android.data.persistence.PersistentCacheStore
@@ -23,6 +24,7 @@ import org.oppia.android.data.persistence.PersistentCacheStore.PublishMode
import org.oppia.android.data.persistence.PersistentCacheStore.UpdateMode
import org.oppia.android.domain.oppialogger.LoggingIdentifierController
import org.oppia.android.domain.oppialogger.OppiaLogger
+import org.oppia.android.domain.oppialogger.analytics.AnalyticsController
import org.oppia.android.domain.oppialogger.analytics.LearnerAnalyticsLogger
import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController
import org.oppia.android.domain.translation.TranslationController
@@ -34,6 +36,7 @@ import org.oppia.android.util.data.DataProviders.Companion.transformAsync
import org.oppia.android.util.locale.OppiaLocale
import org.oppia.android.util.platformparameter.EnableLearnerStudyAnalytics
import org.oppia.android.util.platformparameter.EnableLoggingLearnerStudyIds
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.oppia.android.util.profile.DirectoryManagementUtil
import org.oppia.android.util.profile.ProfileNameValidator
@@ -67,7 +70,6 @@ private const val DELETE_PROFILE_PROVIDER_ID = "delete_profile_provider_id"
private const val SET_CURRENT_PROFILE_ID_PROVIDER_ID = "set_current_profile_id_provider_id"
private const val UPDATE_READING_TEXT_SIZE_PROVIDER_ID =
"update_reading_text_size_provider_id"
-private const val UPDATE_APP_LANGUAGE_PROVIDER_ID = "update_app_language_provider_id"
private const val GET_AUDIO_LANGUAGE_PROVIDER_ID = "get_audio_language_provider_id"
private const val UPDATE_AUDIO_LANGUAGE_PROVIDER_ID = "update_audio_language_provider_id"
private const val UPDATE_LEARNER_ID_PROVIDER_ID = "update_learner_id_provider_id"
@@ -81,6 +83,10 @@ private const val RETRIEVE_LAST_SELECTED_CLASSROOM_ID_PROVIDER_ID =
"retrieve_last_selected_classroom_id_provider_id"
private const val UPDATE_PROFILE_DETAILS_PROVIDER_ID = "update_profile_details_data_provider_id"
private const val UPDATE_PROFILE_TYPE_PROVIDER_ID = "update_profile_type_data_provider_id"
+private const val UPDATE_START_ONBOARDING_FLOW_PROVIDER_ID =
+ "update_start_onboarding_flow_provider_id"
+private const val UPDATE_END_ONBOARDING_FLOW_PROVIDER_ID = "update_end_onboarding_flow_provider_id"
+private const val PROFILE_ONBOARDING_MODE_PROVIDER_ID = "profile_onboarding_mode_data_provider_id"
/** Controller for retrieving, adding, updating, and deleting profiles. */
@Singleton
@@ -100,7 +106,10 @@ class ProfileManagementController @Inject constructor(
@EnableLoggingLearnerStudyIds
private val enableLoggingLearnerStudyIds: PlatformParameterValue,
private val profileNameValidator: ProfileNameValidator,
- private val translationController: TranslationController
+ private val translationController: TranslationController,
+ @EnableOnboardingFlowV2
+ private val enableOnboardingFlowV2: PlatformParameterValue,
+ private val analyticsController: AnalyticsController
) {
private var currentProfileId: Int = DEFAULT_LOGGED_OUT_INTERNAL_PROFILE_ID
private val profileDataStore =
@@ -209,6 +218,11 @@ class ProfileManagementController @Inject constructor(
return profileDataStore.transformAsync(GET_PROFILE_PROVIDER_ID) {
val profile = it.profilesMap[profileId.internalId]
if (profile != null) {
+ if (enableOnboardingFlowV2.value) {
+ if (profile.profileType.equals(ProfileType.PROFILE_TYPE_UNSPECIFIED)) {
+ updateProfileType(profileId, computeProfileType(profile.isAdmin, profile.pin))
+ }
+ }
AsyncResult.Success(profile)
} else {
AsyncResult.Failure(
@@ -322,6 +336,106 @@ class ProfileManagementController @Inject constructor(
}
}
+ private fun computeProfileType(isAdmin: Boolean, pin: String?): ProfileType {
+ return when {
+ isAdminWithPin(isAdmin, pin) -> ProfileType.SUPERVISOR
+ isAdmin -> ProfileType.SOLE_LEARNER
+ else -> ProfileType.ADDITIONAL_LEARNER
+ }
+ }
+
+ private fun isAdminWithPin(isAdmin: Boolean, pin: String?): Boolean {
+ return isAdmin && !pin.isNullOrBlank()
+ }
+
+ /**
+ * Marks that the profile has started the onboarding flow, so that they can skip the profile setup
+ * step if onboarding was previously abandoned.
+ *
+ * @param profileId The ID of the profile to update.
+ * @return A [DataProvider] that represents the result of the update operation.
+ */
+ fun markProfileOnboardingStarted(profileId: ProfileId): DataProvider {
+ val deferred = profileDataStore.storeDataWithCustomChannelAsync(
+ updateInMemoryCache = true
+ ) {
+ val profile =
+ it.profilesMap[profileId.internalId] ?: return@storeDataWithCustomChannelAsync Pair(
+ it,
+ ProfileActionStatus.PROFILE_NOT_FOUND
+ )
+ val updatedProfileBuilder = profile.toBuilder()
+ if (!profile.startedProfileOnboarding) {
+ updatedProfileBuilder.startedProfileOnboarding = true
+ analyticsController.logProfileOnboardingStartedContext(profileId)
+ }
+ val profileDatabaseBuilder = it.toBuilder().putProfiles(
+ profileId.internalId,
+ updatedProfileBuilder.build()
+ )
+ Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS)
+ }
+ return dataProviders.createInMemoryDataProviderAsync(UPDATE_START_ONBOARDING_FLOW_PROVIDER_ID) {
+ return@createInMemoryDataProviderAsync getDeferredResult(profileId, null, deferred)
+ }
+ }
+
+ /**
+ * Marks that the profile has completed the onboarding flow so that the onboarding flow is not
+ * shown after the initial login.
+ *
+ * @param profileId the ID of the profile to update
+ * @return a [DataProvider] that represents the result of the update operation
+ */
+ fun markProfileOnboardingEnded(profileId: ProfileId): DataProvider {
+ val deferred = profileDataStore.storeDataWithCustomChannelAsync(
+ updateInMemoryCache = true
+ ) {
+ val profile =
+ it.profilesMap[profileId.internalId] ?: return@storeDataWithCustomChannelAsync Pair(
+ it,
+ ProfileActionStatus.PROFILE_NOT_FOUND
+ )
+ val updatedProfileBuilder = profile.toBuilder()
+ if (!profile.completedProfileOnboarding) {
+ updatedProfileBuilder.completedProfileOnboarding = true
+ analyticsController.logProfileOnboardingEndedContext(profileId)
+ }
+ val profileDatabaseBuilder = it.toBuilder().putProfiles(
+ profileId.internalId,
+ updatedProfileBuilder.build()
+ )
+ Pair(profileDatabaseBuilder.build(), ProfileActionStatus.SUCCESS)
+ }
+ return dataProviders.createInMemoryDataProviderAsync(UPDATE_END_ONBOARDING_FLOW_PROVIDER_ID) {
+ return@createInMemoryDataProviderAsync getDeferredResult(profileId, null, deferred)
+ }
+ }
+
+ /** Returns the state of the app based on the number and type of existing profiles. */
+ fun getProfileOnboardingMode(): DataProvider {
+ return getProfiles().transform(PROFILE_ONBOARDING_MODE_PROVIDER_ID) { profileList ->
+ val profileCount = profileList.size
+ when {
+ profileCount > 1 -> ProfileOnboardingMode.MULTIPLE_PROFILES
+ profileCount == 1 -> {
+ when (profileList.first().profileType) {
+ ProfileType.SUPERVISOR -> {
+ ProfileOnboardingMode.SUPERVISOR_PROFILE_ONLY
+ }
+ ProfileType.SOLE_LEARNER -> {
+ ProfileOnboardingMode.SOLE_LEARNER_PROFILE_ONLY
+ }
+ else -> {
+ ProfileOnboardingMode.UNKNOWN_PROFILE_TYPE
+ }
+ }
+ }
+ else -> ProfileOnboardingMode.NEW_INSTALL
+ }
+ }
+ }
+
/**
* Updates the profile avatar of an existing profile.
*
diff --git a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt
index c3ef0be50a8..10f3c2df525 100644
--- a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt
@@ -76,6 +76,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.platformparameter.EnableLearnerStudyAnalytics
import org.oppia.android.util.platformparameter.EnableLoggingLearnerStudyIds
import org.oppia.android.util.platformparameter.EnableNpsSurvey
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.robolectric.Shadows
import org.robolectric.annotation.Config
@@ -934,6 +935,12 @@ class AudioPlayerControllerTest {
fun provideEnableNpsSurvey(): PlatformParameterValue {
return PlatformParameterValue.createDefaultParameter(defaultValue = true)
}
+
+ @Provides
+ @EnableOnboardingFlowV2
+ fun provideEnableOnboardingFlowV2(): PlatformParameterValue {
+ return PlatformParameterValue.createDefaultParameter(defaultValue = true)
+ }
}
// TODO(#89): Move this to a common test application component.
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 3026b834567..ad42696a603 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
@@ -116,6 +116,7 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
import org.oppia.android.util.platformparameter.EnableLearnerStudyAnalytics
import org.oppia.android.util.platformparameter.EnableLoggingLearnerStudyIds
import org.oppia.android.util.platformparameter.EnableNpsSurvey
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
@@ -3868,6 +3869,12 @@ class ExplorationProgressControllerTest {
fun provideEnableNpsSurvey(): PlatformParameterValue {
return PlatformParameterValue.createDefaultParameter(defaultValue = true)
}
+
+ @Provides
+ @EnableOnboardingFlowV2
+ fun provideEnableOnboardingFlowV2(): PlatformParameterValue {
+ return PlatformParameterValue.createDefaultParameter(defaultValue = true)
+ }
}
// TODO(#89): Move this to a common test application component.
diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt
index 73c213b9b21..4da145e5a91 100644
--- a/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/OppiaLoggerTest.kt
@@ -18,6 +18,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.BEGIN_SU
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.CLOSE_REVISION_CARD
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.COMPLETE_APP_ONBOARDING
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.CONSOLE_LOG
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.END_PROFILE_ONBOARDING_EVENT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_CONCEPT_CARD
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_EXPLORATION_ACTIVITY
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_HOME
@@ -32,6 +33,8 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STO
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.SHOW_SURVEY_POPUP
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_PROFILE_ONBOARDING_EVENT
+import org.oppia.android.app.model.ProfileId
import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule
import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
import org.oppia.android.testing.FakeAnalyticsEventLogger
@@ -106,6 +109,8 @@ class OppiaLoggerTest {
private val TEST_INFO_EXCEPTION = Throwable(TEST_INFO_LOG_EXCEPTION)
private val TEST_WARN_EXCEPTION = Throwable(TEST_WARN_LOG_EXCEPTION)
private val TEST_ERROR_EXCEPTION = Throwable(TEST_ERROR_LOG_EXCEPTION)
+
+ private val TEST_PROFILE_ID = ProfileId.newBuilder().setInternalId(0).build()
}
@Inject
@@ -420,6 +425,22 @@ class OppiaLoggerTest {
.isEqualTo(TEST_FOREGROUND_TIME.toFloat())
}
+ @Test
+ fun testLogger_createProfileOnboardingStartedContext_returnsCorrectProfileOnboardingContext() {
+ val eventContext = oppiaLogger.createProfileOnboardingStartedContext(TEST_PROFILE_ID)
+
+ assertThat(eventContext.activityContextCase).isEqualTo(START_PROFILE_ONBOARDING_EVENT)
+ assertThat(eventContext.startProfileOnboardingEvent.profileId).isEqualTo(TEST_PROFILE_ID)
+ }
+
+ @Test
+ fun testLogger_createProfileOnboardingEndedContext_returnsCorrectProfileOnboardingContext() {
+ val eventContext = oppiaLogger.createProfileOnboardingEndedContext(TEST_PROFILE_ID)
+
+ assertThat(eventContext.activityContextCase).isEqualTo(END_PROFILE_ONBOARDING_EVENT)
+ assertThat(eventContext.endProfileOnboardingEvent.profileId).isEqualTo(TEST_PROFILE_ID)
+ }
+
private fun setUpTestApplicationComponent() {
DaggerOppiaLoggerTest_TestApplicationComponent.builder()
.setApplication(ApplicationProvider.getApplicationContext())
diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt
index aadb627472f..3017b830a39 100644
--- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt
@@ -1151,6 +1151,32 @@ class AnalyticsControllerTest {
assertThat(fakeAnalyticsEventLogger.getEventListCount()).isEqualTo(3)
}
+ @Test
+ fun testController_lowPriorityEvent_withProfileOnboardingStartedContext_checkLogsEvent() {
+ setUpTestApplicationComponent()
+ val profileId = ProfileId.newBuilder().setInternalId(0).build()
+ analyticsController.logProfileOnboardingStartedContext(profileId = profileId)
+ testCoroutineDispatchers.runCurrent()
+
+ val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(eventLog).hasStartProfileOnboardingContextThat {
+ hasProfileIdThat().isEqualTo(profileId)
+ }
+ }
+
+ @Test
+ fun testController_lowPriorityEvent_withProfileOnboardingEndedContext_checkLogsEvent() {
+ setUpTestApplicationComponent()
+ val profileId = ProfileId.newBuilder().setInternalId(0).build()
+ analyticsController.logProfileOnboardingEndedContext(profileId = profileId)
+ testCoroutineDispatchers.runCurrent()
+
+ val eventLog = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(eventLog).hasEndProfileOnboardingContextThat {
+ hasProfileIdThat().isEqualTo(profileId)
+ }
+ }
+
private fun setUpTestApplicationComponent(enableLearnerStudyAnalytics: Boolean = false) {
TestPlatformParameterModule.forceEnableLearnerStudyAnalytics(enableLearnerStudyAnalytics)
ApplicationProvider.getApplicationContext().inject(this)
diff --git a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt
index 287239d6e72..1abb3c13590 100644
--- a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt
@@ -28,6 +28,7 @@ import org.oppia.android.app.model.AudioLanguage.NIGERIAN_PIDGIN_LANGUAGE
import org.oppia.android.app.model.Profile
import org.oppia.android.app.model.ProfileDatabase
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileOnboardingMode
import org.oppia.android.app.model.ProfileType
import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE
import org.oppia.android.domain.classroom.TEST_CLASSROOM_ID_1
@@ -62,8 +63,10 @@ import org.oppia.android.util.logging.GlobalLogLevel
import org.oppia.android.util.logging.LogLevel
import org.oppia.android.util.logging.SyncStatusModule
import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.oppia.android.util.platformparameter.ENABLE_ONBOARDING_FLOW_V2_DEFAULT_VALUE
import org.oppia.android.util.platformparameter.EnableLearnerStudyAnalytics
import org.oppia.android.util.platformparameter.EnableLoggingLearnerStudyIds
+import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.LEARNER_STUDY_ANALYTICS_DEFAULT_VALUE
import org.oppia.android.util.platformparameter.PlatformParameterValue
import org.oppia.android.util.threading.BackgroundDispatcher
@@ -82,7 +85,8 @@ import javax.inject.Singleton
@LooperMode(LooperMode.Mode.PAUSED)
@Config(application = ProfileManagementControllerTest.TestApplication::class)
class ProfileManagementControllerTest {
- @get:Rule val oppiaTestRule = OppiaTestRule()
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
@Inject lateinit var context: Context
@Inject lateinit var profileTestHelper: ProfileTestHelper
@Inject lateinit var profileManagementController: ProfileManagementController
@@ -122,6 +126,7 @@ class ProfileManagementControllerTest {
@After
fun tearDown() {
TestModule.enableLearnerStudyAnalytics = false
+ TestModule.enableOnboardingFlowV2 = false
}
@Test
@@ -145,6 +150,108 @@ class ProfileManagementControllerTest {
assertThat(profile.lastSelectedClassroomId).isEmpty()
}
+ @Test
+ fun testAddProfile_addSoleLearnerProfile_onboardingV2Enabled_checkProfileIsAdded() {
+ setUpTestWithOnboardingV2Enabled(true)
+ val dataProvider = addAdminProfile(name = "James", pin = "")
+
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val profileDatabase = readProfileDatabase()
+ val profile = profileDatabase.profilesMap[0]!!
+ assertThat(profile.name).isEqualTo("James")
+ assertThat(profile.pin).isEqualTo("")
+ assertThat(profile.allowDownloadAccess).isEqualTo(true)
+ assertThat(profile.id.internalId).isEqualTo(0)
+ assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE)
+ assertThat(profile.numberOfLogins).isEqualTo(0)
+ assertThat(profile.isContinueButtonAnimationSeen).isEqualTo(false)
+ assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue()
+ assertThat(profile.surveyLastShownTimestampMs).isEqualTo(0L)
+ }
+
+ @Test
+ fun testAddProfile_addSupervisorProfile_withPin_onboardingV2Enabled_checkProfileIsAdded() {
+ setUpTestWithOnboardingV2Enabled(true)
+ val dataProvider = addAdminProfile(name = "James")
+
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val profileDatabase = readProfileDatabase()
+ val profile = profileDatabase.profilesMap[0]!!
+ assertThat(profile.name).isEqualTo("James")
+ assertThat(profile.pin).isEqualTo("12345")
+ assertThat(profile.allowDownloadAccess).isEqualTo(true)
+ assertThat(profile.id.internalId).isEqualTo(0)
+ assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE)
+ assertThat(profile.numberOfLogins).isEqualTo(0)
+ assertThat(profile.isContinueButtonAnimationSeen).isEqualTo(false)
+ assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue()
+ assertThat(profile.surveyLastShownTimestampMs).isEqualTo(0L)
+ }
+
+ @Test
+ fun testAddProfile_addAdditionalLearnerProfile_withPin_onboardingV2Enabled_checkProfileIsAdded() {
+ setUpTestWithOnboardingV2Enabled(true)
+ val dataProvider = addNonAdminProfile(name = "James")
+
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val profileDatabase = readProfileDatabase()
+ val profile = profileDatabase.profilesMap[0]!!
+ assertThat(profile.name).isEqualTo("James")
+ assertThat(profile.pin).isEqualTo("12345")
+ assertThat(profile.allowDownloadAccess).isEqualTo(true)
+ assertThat(profile.id.internalId).isEqualTo(0)
+ assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE)
+ assertThat(profile.numberOfLogins).isEqualTo(0)
+ assertThat(profile.isContinueButtonAnimationSeen).isEqualTo(false)
+ assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue()
+ assertThat(profile.surveyLastShownTimestampMs).isEqualTo(0L)
+ }
+
+ @Test
+ fun testAddProfile_addProfile_withPin_onboardingV2Disabled_checkProfileTypeIsNotSet() {
+ setUpTestWithOnboardingV2Enabled(false)
+ val dataProvider = addAdminProfile(name = "James")
+
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val profileDatabase = readProfileDatabase()
+ val profile = profileDatabase.profilesMap[0]!!
+ assertThat(profile.name).isEqualTo("James")
+ assertThat(profile.pin).isEqualTo("12345")
+ assertThat(profile.allowDownloadAccess).isEqualTo(true)
+ assertThat(profile.id.internalId).isEqualTo(0)
+ assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE)
+ assertThat(profile.numberOfLogins).isEqualTo(0)
+ assertThat(profile.isContinueButtonAnimationSeen).isEqualTo(false)
+ assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue()
+ assertThat(profile.surveyLastShownTimestampMs).isEqualTo(0L)
+ assertThat(profile.profileType).isEqualTo(ProfileType.PROFILE_TYPE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testAddProfile_addProfile_withoutPin_onboardingV2Disabled_checkProfileTypeIsNotSet() {
+ setUpTestWithOnboardingV2Enabled(false)
+ val dataProvider = addAdminProfile(name = "James", pin = "")
+
+ monitorFactory.waitForNextSuccessfulResult(dataProvider)
+
+ val profileDatabase = readProfileDatabase()
+ val profile = profileDatabase.profilesMap[0]!!
+ assertThat(profile.name).isEqualTo("James")
+ assertThat(profile.pin).isEqualTo("")
+ assertThat(profile.allowDownloadAccess).isEqualTo(true)
+ assertThat(profile.id.internalId).isEqualTo(0)
+ assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE)
+ assertThat(profile.numberOfLogins).isEqualTo(0)
+ assertThat(profile.isContinueButtonAnimationSeen).isEqualTo(false)
+ assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue()
+ assertThat(profile.surveyLastShownTimestampMs).isEqualTo(0L)
+ assertThat(profile.profileType).isEqualTo(ProfileType.PROFILE_TYPE_UNSPECIFIED)
+ }
+
@Test
fun testAddProfile_addProfile_studyOff_checkProfileDoesNotIncludeLearnerId() {
setUpTestApplicationComponentWithoutLearnerAnalyticsStudy()
@@ -1619,6 +1726,205 @@ class ProfileManagementControllerTest {
assertThat(failure).hasMessageThat().isEqualTo("ProfileType must be set.")
}
+ @Test
+ fun testProfileMigration_getExistingNonAdminProfile_checkProfileTypeIsAdditionalLearner() {
+ // Simulate profiles already created in a previous app instance.
+ executeInPreviousAppInstance { testComponent ->
+ testComponent.getProfileManagementController().addProfile(
+ name = "Admin",
+ isAdmin = true,
+ allowDownloadAccess = true,
+ pin = "12345",
+ colorRgb = -1,
+ avatarImagePath = null
+ )
+ testComponent.getProfileManagementController().addProfile(
+ name = "John",
+ isAdmin = false,
+ allowDownloadAccess = true,
+ pin = "",
+ colorRgb = -1,
+ avatarImagePath = null
+ )
+ testComponent.getTestCoroutineDispatchers().runCurrent()
+ }
+
+ setUpTestWithOnboardingV2Enabled(true)
+ val getProfileProvider = profileManagementController.getProfile(PROFILE_ID_1)
+ val profile = monitorFactory.waitForNextSuccessfulResult(getProfileProvider)
+ assertThat(profile.profileType).isEqualTo(ProfileType.ADDITIONAL_LEARNER)
+ }
+
+ @Test
+ fun testProfileMigration_getExistingAdminWithPin_checkProfileTypeIsSupervisor() {
+ // Simulate profiles already created in a previous app instance.
+ executeInPreviousAppInstance { testComponent ->
+ testComponent.getProfileManagementController().addProfile(
+ name = "Admin",
+ isAdmin = true,
+ allowDownloadAccess = true,
+ pin = "12345",
+ colorRgb = -1,
+ avatarImagePath = null
+ )
+ testComponent.getProfileManagementController().addProfile(
+ name = "John",
+ isAdmin = false,
+ allowDownloadAccess = true,
+ pin = "",
+ colorRgb = -1,
+ avatarImagePath = null
+ )
+ testComponent.getTestCoroutineDispatchers().runCurrent()
+ }
+
+ setUpTestWithOnboardingV2Enabled(true)
+ val getProfileProvider = profileManagementController.getProfile(PROFILE_ID_0)
+ val profile = monitorFactory.waitForNextSuccessfulResult(getProfileProvider)
+ assertThat(profile.profileType).isEqualTo(ProfileType.SUPERVISOR)
+ }
+
+ @Test
+ fun testProfileMigration_getExistingAdminWithoutPin_checkProfileTypeIsSoleLearner() {
+ // Simulate profiles already created in a previous app instance.
+ executeInPreviousAppInstance { testComponent ->
+ testComponent.getProfileManagementController().addProfile(
+ name = "Admin",
+ isAdmin = true,
+ allowDownloadAccess = true,
+ pin = "",
+ colorRgb = -1,
+ avatarImagePath = null
+ )
+ testComponent.getTestCoroutineDispatchers().runCurrent()
+ }
+
+ setUpTestWithOnboardingV2Enabled(true)
+ val getProfileProvider = profileManagementController.getProfile(PROFILE_ID_0)
+ val profile = monitorFactory.waitForNextSuccessfulResult(getProfileProvider)
+ assertThat(profile.profileType).isEqualTo(ProfileType.SOLE_LEARNER)
+ }
+
+ @Test
+ fun testProfileOnboardingState_oneAdminProfileWithoutPassword_returnsSoleLeanerTypeMode() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfileAndWait(name = "James", pin = "")
+
+ val updateProfileProvider =
+ profileManagementController.updateProfileType(ADMIN_PROFILE_ID_0, ProfileType.SOLE_LEARNER)
+ monitorFactory.ensureDataProviderExecutes(updateProfileProvider)
+
+ val profileOnboardingModeProvider = profileManagementController.getProfileOnboardingMode()
+ val profileOnboardingModeResult =
+ monitorFactory.waitForNextSuccessfulResult(profileOnboardingModeProvider)
+
+ assertThat(profileOnboardingModeResult).isEqualTo(
+ ProfileOnboardingMode.SOLE_LEARNER_PROFILE_ONLY
+ )
+ }
+
+ @Test
+ fun testProfileOnboardingState_oneAdminProfileWithPassword_returnsAdminOnlyMode() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfileAndWait(name = "James")
+
+ val updateProfileProvider =
+ profileManagementController.updateProfileType(ADMIN_PROFILE_ID_0, ProfileType.SUPERVISOR)
+ monitorFactory.ensureDataProviderExecutes(updateProfileProvider)
+
+ val profileOnboardingModeProvider = profileManagementController.getProfileOnboardingMode()
+ val profileOnboardingModeResult =
+ monitorFactory.waitForNextSuccessfulResult(profileOnboardingModeProvider)
+
+ assertThat(profileOnboardingModeResult).isEqualTo(ProfileOnboardingMode.SUPERVISOR_PROFILE_ONLY)
+ }
+
+ @Test
+ fun testProfileOnboardingState_multipleProfiles_returnsMultipleProfilesTypeMode() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfileAndWait(name = "James")
+ addNonAdminProfileAndWait(name = "Rajat", pin = "01234")
+ addNonAdminProfileAndWait(name = "Rohit", pin = "")
+
+ val profileOnboardingModeProvider = profileManagementController.getProfileOnboardingMode()
+ val profileOnboardingModeResult =
+ monitorFactory.waitForNextSuccessfulResult(profileOnboardingModeProvider)
+
+ assertThat(profileOnboardingModeResult).isEqualTo(ProfileOnboardingMode.MULTIPLE_PROFILES)
+ }
+
+ @Test
+ fun testProfileOnboardingState_noProfilesFound_returnsNewInstallTypeMode() {
+ setUpTestWithOnboardingV2Enabled(true)
+
+ val profileOnboardingModeProvider = profileManagementController.getProfileOnboardingMode()
+ val profileOnboardingModeResult =
+ monitorFactory.waitForNextSuccessfulResult(profileOnboardingModeProvider)
+
+ assertThat(profileOnboardingModeResult).isEqualTo(ProfileOnboardingMode.NEW_INSTALL)
+ }
+
+ @Test
+ fun testProfileOnboardingState_existingProfilesV1_returnsUnknownProfileTypeMode() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfileAndWait(name = "James")
+
+ val profileOnboardingModeProvider = profileManagementController.getProfileOnboardingMode()
+ val profileOnboardingModeResult =
+ monitorFactory.waitForNextSuccessfulResult(profileOnboardingModeProvider)
+
+ assertThat(profileOnboardingModeResult).isEqualTo(ProfileOnboardingMode.UNKNOWN_PROFILE_TYPE)
+ }
+
+ @Test
+ fun testGetProfile_createAdmin_returnsSupervisorType() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfile(name = "James")
+ val profile = retrieveProfile(PROFILE_ID_0)
+ assertThat(profile.profileType).isEqualTo(ProfileType.SUPERVISOR)
+ }
+
+ @Test
+ fun testGetProfile_createSoleLearner_returnsSoleLearnerType() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfile(name = "James", pin = "")
+ val profile = retrieveProfile(PROFILE_ID_0)
+ assertThat(profile.profileType).isEqualTo(ProfileType.SOLE_LEARNER)
+ }
+
+ @Test
+ fun testGetProfile_createAdditionalLearner_returnsAdditionalLearnerType() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfile(name = "James")
+ addNonAdminProfile(name = "Rajat")
+ val profile = retrieveProfile(PROFILE_ID_1)
+ assertThat(profile.profileType).isEqualTo(ProfileType.ADDITIONAL_LEARNER)
+ }
+
+ @Test
+ fun testProfileOnboarding_markOnboardingStarted_logsStartProfileOnboardingEvent() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfile(name = "James", pin = "")
+ val onboardingProvider = profileManagementController.markProfileOnboardingStarted(PROFILE_ID_0)
+ monitorFactory.ensureDataProviderExecutes(onboardingProvider)
+ val event = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(event).hasStartProfileOnboardingContextThat {
+ hasProfileIdThat().isEqualTo(PROFILE_ID_0)
+ }
+ }
+
+ @Test
+ fun testProfileOnboarding_markOnboardingCompleted_logsEndProfileOnboardingEvent() {
+ setUpTestWithOnboardingV2Enabled(true)
+ addAdminProfile(name = "James", pin = "")
+ val onboardingProvider = profileManagementController.markProfileOnboardingEnded(PROFILE_ID_0)
+ monitorFactory.ensureDataProviderExecutes(onboardingProvider)
+ val event = fakeAnalyticsEventLogger.getMostRecentEvent()
+ assertThat(event).hasEndProfileOnboardingContextThat {
+ hasProfileIdThat().isEqualTo(PROFILE_ID_0)
+ }
+ }
+
private fun addTestProfiles() {
val profileAdditionProviders = PROFILES_LIST.map {
addNonAdminProfile(it.name, pin = it.pin, allowDownloadAccess = it.allowDownloadAccess)
@@ -1766,10 +2072,28 @@ class ProfileManagementControllerTest {
setUpTestApplicationComponent()
}
+ private fun setUpTestWithOnboardingV2Enabled(enableOnboardingV2: Boolean) {
+ TestModule.enableOnboardingFlowV2 = enableOnboardingV2
+ setUpTestApplicationComponent()
+ }
+
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
+ private fun executeInPreviousAppInstance(block: (TestApplicationComponent) -> Unit) {
+ val testApplication = TestApplication()
+ // The true application is hooked as a base context. This is to make sure the new application
+ // can behave like a real Android application class (per Robolectric) without having a shared
+ // Dagger dependency graph with the application under test.
+ testApplication.attachBaseContext(ApplicationProvider.getApplicationContext())
+ block(
+ DaggerProfileManagementControllerTest_TestApplicationComponent.builder()
+ .setApplication(testApplication)
+ .build()
+ )
+ }
+
// TODO(#89): Move this to a common test application component.
@Module
class TestModule {
@@ -1777,6 +2101,7 @@ class ProfileManagementControllerTest {
// This is expected to be off by default, so this helps the tests above confirm that the
// feature's default value is, indeed, off.
var enableLearnerStudyAnalytics = LEARNER_STUDY_ANALYTICS_DEFAULT_VALUE
+ var enableOnboardingFlowV2 = ENABLE_ONBOARDING_FLOW_V2_DEFAULT_VALUE
}
@Provides
@@ -1822,6 +2147,16 @@ class ProfileManagementControllerTest {
defaultValue = enableFeature
)
}
+
+ @Provides
+ @EnableOnboardingFlowV2
+ fun provideEnableOnboardingFlowV2(): PlatformParameterValue {
+ // Snapshot the value so that it doesn't change between injection and use.
+ val enableFeature = enableOnboardingFlowV2
+ return PlatformParameterValue.createDefaultParameter(
+ defaultValue = enableFeature
+ )
+ }
}
@Module
@@ -1856,6 +2191,10 @@ class ProfileManagementControllerTest {
}
fun inject(profileManagementControllerTest: ProfileManagementControllerTest)
+
+ fun getProfileManagementController(): ProfileManagementController
+
+ fun getTestCoroutineDispatchers(): TestCoroutineDispatchers
}
class TestApplication : Application(), DataProvidersInjectorProvider {
@@ -1869,6 +2208,10 @@ class ProfileManagementControllerTest {
component.inject(profileManagementControllerTest)
}
+ public override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ }
+
override fun getDataProvidersInjector(): DataProvidersInjector = component
}
}
diff --git a/model/src/main/proto/arguments.proto b/model/src/main/proto/arguments.proto
index ac21f121a5d..8540563d3ee 100644
--- a/model/src/main/proto/arguments.proto
+++ b/model/src/main/proto/arguments.proto
@@ -15,6 +15,9 @@ option java_multiple_files = true;
message ExitProfileDialogArguments {
// Decides the correct menu item to be highlighted after canceling the ExitProfileDialogFragment.
HighlightItem highlight_item = 1;
+
+ // Decides the exit pathway depending on a user's profile type.
+ ProfileType profile_type = 2;
}
// Represents the type of item/menuItem that should be highlighted after canceling the
@@ -913,3 +916,15 @@ message OnboardingFragmentStateBundle {
// The current selected language.
OppiaLanguage selected_language = 1;
}
+
+// Params required when creating a new ProfileChooserActivity.
+message ProfileChooserActivityParams {
+ // The ProfileType of the new profile as implied by the user's selection.
+ ProfileType profile_type = 1;
+}
+
+// Arguments required when creating a new ProfileChooserFragment.
+message ProfileChooserFragmentArguments {
+ // The ProfileType of the new profile as implied by the user's selection.
+ ProfileType profile_type = 1;
+}
diff --git a/model/src/main/proto/oppia_logger.proto b/model/src/main/proto/oppia_logger.proto
index a34f404aa07..3cab9be1cd6 100644
--- a/model/src/main/proto/oppia_logger.proto
+++ b/model/src/main/proto/oppia_logger.proto
@@ -38,6 +38,11 @@ message EventLog {
// The audio language selection context at the time of this event's creation.
AudioTranslationLanguageSelection audio_translation_language_selection = 7;
+ // The profileId and profileType to which this event corresponds, or empty if this event is not tied to a particular
+ // profile. This is only used for diagnostic purposes as events are only ever logged anonymously
+ // at source.
+ ProfileContext profile_context = 9;
+
// Structure of an activity context.
message Context {
// Deprecated exploration context. This is now handled via the open_exploration_activity context
@@ -222,9 +227,29 @@ message EventLog {
// The event being logged is related to viewing a solution that was already unlocked.
ExplorationContext view_existing_solution_context = 55;
+
+ // The event being logged indicates that the profile user has started going through the
+ // onboarding flow.
+ ProfileOnboardingContext start_profile_onboarding_event = 57;
+
+ // The event being logged indicates that the profile user has reached the home screen for the
+ // first time.
+ ProfileOnboardingContext end_profile_onboarding_event = 58;
}
}
+ // Structure of a ProfileContext which contains the profileId and profileType to which this event
+ // corresponds.
+ message ProfileContext {
+ // The profile to which this event corresponds, or empty if this event is not tied to a particular
+ // profile. This is only used for diagnostic purposes as events are only ever logged anonymously
+ // at source.
+ ProfileId profile_id = 1;
+
+ // Represents the type of user profile.
+ ProfileType profile_type = 2;
+ }
+
// Structure of a question context.
message QuestionContext {
// The active question ID when the event is logged.
@@ -505,6 +530,12 @@ message EventLog {
PlatformParameter.SyncStatus flag_sync_status = 3;
}
+ // Structure for the profile onboarding context.
+ message ProfileOnboardingContext {
+ // The Id of the profile to be onboarded.
+ ProfileId profile_id = 1;
+ }
+
// Supported priority of events for event logging
enum Priority {
// The undefined priority of an event
diff --git a/model/src/main/proto/profile.proto b/model/src/main/proto/profile.proto
index bb55c8b2b47..11755096bc4 100644
--- a/model/src/main/proto/profile.proto
+++ b/model/src/main/proto/profile.proto
@@ -93,6 +93,12 @@ message Profile {
// Represents the type of user which informs the configuration options available to them.
ProfileType profile_type = 20;
+
+ // Indicates that this profile has viewed the relevant onboarding introduction screen.
+ bool started_profile_onboarding = 21;
+
+ // Indicates that this profile has reached the home screen for the first time.
+ bool completed_profile_onboarding = 22;
}
// Represents the type of user using the app.
@@ -163,3 +169,25 @@ enum AudioLanguage {
ARABIC_LANGUAGE = 7;
NIGERIAN_PIDGIN_LANGUAGE = 8;
}
+
+// Indicates the state of the app with regards to the number and type of existing profiles.
+enum ProfileOnboardingMode {
+ // Indicates that the number or type of profiles is unknown.
+ PROFILE_ONBOARDING_MODE_UNSPECIFIED = 0;
+
+ // Indicates that this is a new app install given that there are no existing profiles.
+ NEW_INSTALL = 1;
+
+ // Indicates that there is only one profile and it is a sole learner profile.
+ SOLE_LEARNER_PROFILE_ONLY = 2;
+
+ // Indicates that there is only one profile and it is an admin profile.
+ SUPERVISOR_PROFILE_ONLY = 3;
+
+ // Indicates that there are multiple profiles on the device.
+ MULTIPLE_PROFILES = 4;
+
+ // Indicates that there is only one profile and the profile type is unknown, indicating that
+ // migration is required.
+ UNKNOWN_PROFILE_TYPE = 5;
+}
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 1ea6be33967..08f1cf99f8e 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -958,6 +958,10 @@ 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/home/ExitProfileListener.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/home/HomeActivity.kt"
source_file_is_incompatible_with_code_coverage: 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 2100e3028b8..544ec39ee9c 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
@@ -22,6 +22,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.BEGIN_SU
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.CLOSE_REVISION_CARD
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.DELETE_PROFILE_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.END_CARD_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.END_PROFILE_ONBOARDING_EVENT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.EXIT_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.FINISH_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.HINT_UNLOCKED_CONTEXT
@@ -55,6 +56,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_OVER_EXPLORATION_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_PROFILE_ONBOARDING_EVENT
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
@@ -1325,6 +1327,58 @@ class EventLogSubject private constructor(
hasResumeLessonSubmitIncorrectAnswerContextThat().block()
}
+ /**
+ * Verifies that the [EventLog] under test has a context corresponding to
+ * [START_PROFILE_ONBOARDING_EVENT] (per [EventLog.Context.getActivityContextCase]).
+ */
+ fun hasStartProfileOnboardingContext() {
+ assertThat(actual.context.activityContextCase).isEqualTo(START_PROFILE_ONBOARDING_EVENT)
+ }
+
+ /**
+ * Verifies the [EventLog]'s context per [hasStartProfileOnboardingContext] and returns a
+ * [ProfileOnboardingContextSubject] to test the corresponding context.
+ */
+ fun hasStartProfileOnboardingContextThat(): ProfileOnboardingContextSubject {
+ hasStartProfileOnboardingContext()
+ return ProfileOnboardingContextSubject.assertThat(
+ actual.context.startProfileOnboardingEvent
+ )
+ }
+
+ /** Verifies the [EventLog]'s context and executes [block]. */
+ fun hasStartProfileOnboardingContextThat(
+ block: ProfileOnboardingContextSubject.() -> Unit
+ ) {
+ hasStartProfileOnboardingContextThat().block()
+ }
+
+ /**
+ * Verifies that the [EventLog] under test has a context corresponding to
+ * [END_PROFILE_ONBOARDING_EVENT] (per [EventLog.Context.getActivityContextCase]).
+ */
+ fun hasEndProfileOnboardingContext() {
+ assertThat(actual.context.activityContextCase).isEqualTo(END_PROFILE_ONBOARDING_EVENT)
+ }
+
+ /**
+ * Verifies the [EventLog]'s context per [hasEndProfileOnboardingContext] and returns a
+ * [ProfileOnboardingContextSubject] to test the corresponding context.
+ */
+ fun hasEndProfileOnboardingContextThat(): ProfileOnboardingContextSubject {
+ hasEndProfileOnboardingContext()
+ return ProfileOnboardingContextSubject.assertThat(
+ actual.context.endProfileOnboardingEvent
+ )
+ }
+
+ /** Verifies the [EventLog]'s context and executes [block]. */
+ fun hasEndProfileOnboardingContextThat(
+ block: ProfileOnboardingContextSubject.() -> Unit
+ ) {
+ hasEndProfileOnboardingContextThat().block()
+ }
+
/**
* Truth subject for verifying properties of [AppLanguageSelection]s.
*
@@ -2400,6 +2454,36 @@ class EventLogSubject private constructor(
}
}
+ /**
+ * Truth subject for verifying properties of [EventLog.ProfileOnboardingContext]s.
+ *
+ * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying
+ * [EventLog.ProfileOnboardingContext] proto can be verified through inherited methods.
+ *
+ * Call [ProfileOnboardingContextSubject.assertThat] to create the subject.
+ */
+ class ProfileOnboardingContextSubject private constructor(
+ metadata: FailureMetadata,
+ private val actual: EventLog.ProfileOnboardingContext
+ ) : LiteProtoSubject(metadata, actual) {
+ /**
+ * Returns a [LiteProtoSubject] to test [EventLog.ProfileOnboardingContext.getProfileId].
+ *
+ * This method never fails since the underlying property defaults to empty string if it's not
+ * defined in the context.
+ */
+ fun hasProfileIdThat(): LiteProtoSubject = LiteProtoTruth.assertThat(actual.profileId)
+
+ companion object {
+ /**
+ * Returns a new [ProfileOnboardingContextSubject] to verify aspects of the specified
+ * [EventLog.ProfileOnboardingContext] value.
+ */
+ fun assertThat(actual: EventLog.ProfileOnboardingContext): ProfileOnboardingContextSubject =
+ assertAbout(::ProfileOnboardingContextSubject).that(actual)
+ }
+ }
+
companion object {
/** Returns a new [EventLogSubject] to verify aspects of the specified [EventLog] value. */
fun assertThat(actual: EventLog): EventLogSubject = assertAbout(::EventLogSubject).that(actual)
diff --git a/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt
index a5e877fa705..59abf05d6cb 100644
--- a/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt
+++ b/testing/src/main/java/org/oppia/android/testing/profile/ProfileTestHelper.kt
@@ -1,6 +1,7 @@
package org.oppia.android.testing.profile
import org.oppia.android.app.model.ProfileId
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.testing.data.DataProviderTestMonitor
import org.oppia.android.util.data.AsyncResult
@@ -64,6 +65,16 @@ class ProfileTestHelper @Inject constructor(
return monitorFactory.createMonitor(logIntoAdmin()).waitForNextResult()
}
+ /** Creates one admin profile without pin and logs in to the profile. */
+ fun addOnlyAdminProfileWithoutPin() {
+ addProfileAndWait(
+ name = "Admin",
+ pin = "",
+ allowDownloadAccess = true,
+ isAdmin = true
+ )
+ }
+
/** Create [numProfiles] number of user profiles. */
fun addMoreProfiles(numProfiles: Int) {
for (x in 0 until numProfiles) {
@@ -104,6 +115,21 @@ class ProfileTestHelper @Inject constructor(
)
}
+ /** Marks a profile as having finished the onboarding flow. */
+ fun markProfileOnboardingEnded(profileId: ProfileId): DataProvider {
+ return profileManagementController.markProfileOnboardingEnded(profileId)
+ }
+
+ /** Marks a profile as having started the onboarding flow. */
+ fun markProfileOnboardingStarted(profileId: ProfileId): DataProvider {
+ return profileManagementController.markProfileOnboardingStarted(profileId)
+ }
+
+ /** Updates the [ProfileType] of an existing profile. */
+ fun updateProfileType(profileId: ProfileId, profileType: ProfileType): DataProvider {
+ return profileManagementController.updateProfileType(profileId, profileType)
+ }
+
/** Returns the continue button animation seen for profile. */
fun getContinueButtonAnimationSeenStatus(profileId: ProfileId): Boolean {
return monitorFactory.waitForNextSuccessfulResult(
diff --git a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt
index 74c9ab3846c..abe33ac86a7 100644
--- a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt
+++ b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt
@@ -12,6 +12,7 @@ import dagger.Provides
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.oppia.android.app.model.ProfileType
import org.oppia.android.domain.oppialogger.LogStorageModule
import org.oppia.android.domain.oppialogger.LoggingIdentifierModule
import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule
@@ -138,6 +139,47 @@ class ProfileTestHelperTest {
assertThat(profileManagementController.getCurrentProfileId()?.internalId).isEqualTo(2)
}
+ @Test
+ fun testLogIntoAdmin_addOnlyAdminProfileWithoutPin_logIntoAdminWithoutPin_checkIsSuccessful() {
+ profileTestHelper.addOnlyAdminProfileWithoutPin()
+ val loginProvider = profileTestHelper.logIntoAdmin()
+ monitorFactory.waitForNextSuccessfulResult(loginProvider)
+ assertThat(profileManagementController.getCurrentProfileId()?.internalId).isEqualTo(0)
+ }
+
+ @Test
+ fun testProfileOnboarding_markOnboardingStarted_checkIsSuccessful() {
+ profileTestHelper.addOnlyAdminProfile()
+ val profileId = profileManagementController.getCurrentProfileId()
+ val onboardingProvider = profileTestHelper.markProfileOnboardingStarted(profileId!!)
+ monitorFactory.waitForNextSuccessfulResult(onboardingProvider)
+ }
+
+ @Test
+ fun testProfileOnboarding_markOnboardingCompleted_checkIsSuccessful() {
+ profileTestHelper.addOnlyAdminProfile()
+ val profileId = profileManagementController.getCurrentProfileId()
+ val onboardingProvider = profileTestHelper.markProfileOnboardingEnded(profileId!!)
+ monitorFactory.waitForNextSuccessfulResult(onboardingProvider)
+ }
+
+ @Test
+ fun testUpdateProfile_updateProfileType_profileTypeShouldBeUpdated() {
+ profileTestHelper.addOnlyAdminProfile()
+ val profileId = profileManagementController.getCurrentProfileId()
+ val updateProvider = profileTestHelper.updateProfileType(profileId!!, ProfileType.SUPERVISOR)
+ monitorFactory.ensureDataProviderExecutes(updateProvider)
+
+ val profilesProvider = profileManagementController.getProfiles()
+ testCoroutineDispatchers.runCurrent()
+
+ val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider)
+ assertThat(profiles.size).isEqualTo(1)
+ assertThat(profiles[0].name).isEqualTo("Admin")
+ assertThat(profiles[0].isAdmin).isTrue()
+ assertThat(profiles[0].profileType).isEqualTo(ProfileType.SUPERVISOR)
+ }
+
// TODO(#89): Move this to a common test application component.
@Module
class TestModule {
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 dde89bc818b..7078750e2ce 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
@@ -18,6 +18,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.COMPLETE
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.CONSOLE_LOG
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.DELETE_PROFILE_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.END_CARD_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.END_PROFILE_ONBOARDING_EVENT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.EXIT_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.FEATURE_FLAG_LIST_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.FINISH_EXPLORATION_CONTEXT
@@ -54,6 +55,7 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.SOLUTION
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_CARD_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_EXPLORATION_CONTEXT
import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_OVER_EXPLORATION_CONTEXT
+import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.START_PROFILE_ONBOARDING_EVENT
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
@@ -86,6 +88,7 @@ import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.Hi
import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.LearnerDetailsContext
import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.MandatorySurveyResponseContext
import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.OptionalSurveyResponseContext
+import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.ProfileOnboardingContext
import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.QuestionContext
import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.RetrofitCallContext
import org.oppia.android.util.logging.EventBundleCreator.EventActivityContext.RetrofitCallFailedContext
@@ -120,6 +123,7 @@ import org.oppia.android.app.model.EventLog.HintContext as HintEventContext
import org.oppia.android.app.model.EventLog.LearnerDetailsContext as LearnerDetailsEventContext
import org.oppia.android.app.model.EventLog.MandatorySurveyResponseContext as MandatorySurveyResponseEventContext
import org.oppia.android.app.model.EventLog.OptionalSurveyResponseContext as OptionalSurveyResponseEventContext
+import org.oppia.android.app.model.EventLog.ProfileOnboardingContext as ProfileOnboardingEventContext
import org.oppia.android.app.model.EventLog.QuestionContext as QuestionEventContext
import org.oppia.android.app.model.EventLog.RetrofitCallContext as RetrofitCallEventContext
import org.oppia.android.app.model.EventLog.RetrofitCallFailedContext as RetrofitCallFailedEventContext
@@ -279,6 +283,10 @@ class EventBundleCreator @Inject constructor(
FEATURE_FLAG_LIST_CONTEXT -> FeatureFlagContext(activityName, featureFlagListContext)
INSTALL_ID_FOR_FAILED_ANALYTICS_LOG ->
SensitiveStringContext(activityName, installIdForFailedAnalyticsLog, "install_id")
+ START_PROFILE_ONBOARDING_EVENT ->
+ ProfileOnboardingContext(activityName, startProfileOnboardingEvent)
+ END_PROFILE_ONBOARDING_EVENT ->
+ ProfileOnboardingContext(activityName, endProfileOnboardingEvent)
ACTIVITYCONTEXT_NOT_SET, null -> EmptyContext(activityName) // No context to create here.
}
}
@@ -691,6 +699,16 @@ class EventBundleCreator @Inject constructor(
store.putNonSensitiveValue("feature_flag_sync_statuses", featureFlagSyncStatuses)
}
}
+
+ /** The [EventActivityContext] corresponding to [ProfileOnboardingEventContext]s. */
+ class ProfileOnboardingContext(
+ activityName: String,
+ value: ProfileOnboardingEventContext
+ ) : EventActivityContext(activityName, value) {
+ override fun ProfileOnboardingEventContext.storeValue(store: PropertyStore) {
+ store.putNonSensitiveValue("profile_id", profileId)
+ }
+ }
}
/** Represents an [OppiaMetricLog] loggable metric (denoted by [LoggableMetricTypeCase]). */
diff --git a/utility/src/main/java/org/oppia/android/util/logging/EventTypeToHumanReadableNameConverter.kt b/utility/src/main/java/org/oppia/android/util/logging/EventTypeToHumanReadableNameConverter.kt
index 687e08dcc92..dbaad083e47 100644
--- a/utility/src/main/java/org/oppia/android/util/logging/EventTypeToHumanReadableNameConverter.kt
+++ b/utility/src/main/java/org/oppia/android/util/logging/EventTypeToHumanReadableNameConverter.kt
@@ -82,6 +82,8 @@ class EventTypeToHumanReadableNameConverter @Inject constructor() {
ActivityContextCase.RETROFIT_CALL_CONTEXT -> "retrofit_call_context"
ActivityContextCase.RETROFIT_CALL_FAILED_CONTEXT -> "retrofit_call_failed_context"
ActivityContextCase.APP_IN_FOREGROUND_TIME -> "app_in_foreground_time"
+ ActivityContextCase.START_PROFILE_ONBOARDING_EVENT -> "start_profile_onboarding_event"
+ ActivityContextCase.END_PROFILE_ONBOARDING_EVENT -> "end_profile_onboarding_event"
}
}
}