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