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" } } }