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 19da63a9d68..c02427393eb 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 @@ -37,6 +38,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 @@ -50,6 +52,9 @@ import org.oppia.android.app.model.AppStartupState 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.ProfileId +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 @@ -66,6 +71,8 @@ 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 @@ -88,8 +95,11 @@ class ClassroomListFragmentPresenter @Inject constructor( private val machineLocale: OppiaLocale.MachineLocale, private val appStartupStateController: AppStartupStateController, private val analyticsController: AnalyticsController, + @EnableOnboardingFlowV2 + private val enableOnboardingFlowV2: PlatformParameterValue ) { 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 @@ -134,7 +144,8 @@ class ClassroomListFragmentPresenter @Inject constructor( sender: ObservableList, positionStart: Int, itemCount: Int - ) {} + ) { + } override fun onItemRangeInserted( sender: ObservableList, @@ -149,17 +160,23 @@ class ClassroomListFragmentPresenter @Inject constructor( fromPosition: Int, toPosition: Int, itemCount: Int - ) {} + ) { + } override fun onItemRangeRemoved( sender: ObservableList, positionStart: Int, itemCount: Int - ) {} + ) { + } } ) - logAppOnboardedEvent() + if (enableOnboardingFlowV2.value) { + subscribeToProfileResult(profileId) + } else { + logAppOnboardedEvent(profileId) + } return binding.root } @@ -265,7 +282,7 @@ class ClassroomListFragmentPresenter @Inject constructor( } } - private fun logAppOnboardedEvent() { + private fun logAppOnboardedEvent(profileId: ProfileId) { val startupStateProvider = appStartupStateController.getAppStartupState() val liveData = startupStateProvider.toLiveData() liveData.observe( @@ -274,7 +291,7 @@ class ClassroomListFragmentPresenter @Inject constructor( override fun onChanged(startUpStateResult: AsyncResult?) { when (startUpStateResult) { null, is AsyncResult.Pending -> { - // Do nothing. + // Do nothing } is AsyncResult.Success -> { liveData.removeObserver(this) @@ -297,12 +314,62 @@ class ClassroomListFragmentPresenter @Inject constructor( ) } + private fun subscribeToProfileResult(profileId: ProfileId) { + profileManagementController.getProfile(profileId).toLiveData().observe(fragment) { + processProfileResult(it) + } + } + + private fun processProfileResult(result: AsyncResult) { + when (result) { + is AsyncResult.Success -> { + val profile = result.value + handleProfileOnboardingState(profile) + handleBackPress(profile.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 handleProfileOnboardingState(profile: Profile) { + // App onboarding is completed by the first profile on the app(SOLE_LEARNER or SUPERVISOR), + // while profile onboarding is completed by each profile. + if (!profile.completedProfileOnboarding) { + profileManagementController.markProfileOnboardingEnded(profileId) + if (profile.profileType == ProfileType.SOLE_LEARNER || + profile.profileType == ProfileType.SUPERVISOR + ) { + appStartupStateController.markOnboardingFlowCompleted() + logAppOnboardedEvent(profileId) + } + } + } + private fun logHomeActivityEvent() { analyticsController.logImportantEvent( oppiaLogger.createOpenHomeContext(), profileId ) } + + private fun handleBackPress(profileType: ProfileType) { + activity.onBackPressedDispatcher.addCallback( + fragment, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + exitProfileListener.exitProfile(profileType) + } + } + ) + } } /** 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 7900163e9a5..a1e67ba9da6 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,12 +73,17 @@ class ExitProfileDialogFragment : InjectableDialogFragment() { dialog.dismiss() } .setPositiveButton(R.string.home_activity_back_dialog_exit) { _, _ -> - // TODO(#3641): Investigate on using finish instead of intent. - val intent = ProfileChooserActivity.createProfileChooserActivity(activity!!) - if (!restoreLastCheckedItem) { - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + if (soleLearnerProfile) { + requireActivity().finish() + } else { + // TODO(#3641): Investigate on using finish instead of intent. + 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 b3ef5d04e3f..b28a3b6fc93 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.lifecycle.Observer @@ -14,7 +15,9 @@ 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.AppStartupState +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 @@ -35,6 +38,8 @@ 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 @@ -53,18 +58,24 @@ class HomeFragmentPresenter @Inject constructor( private val dateTimeUtil: DateTimeUtil, private val translationController: TranslationController, private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, - private val appStartupStateController: AppStartupStateController + private val appStartupStateController: AppStartupStateController, + @EnableOnboardingFlowV2 + private val enableOnboardingFlowV2: PlatformParameterValue ) { 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() @@ -103,12 +114,16 @@ class HomeFragmentPresenter @Inject constructor( it.viewModel = homeViewModel } - logAppOnboardedEvent() + if (enableOnboardingFlowV2.value) { + subscribeToProfileResult(profileId) + } else { + logAppOnboardedEvent(profileId) + } return binding.root } - private fun logAppOnboardedEvent() { + private fun logAppOnboardedEvent(profileId: ProfileId) { val startupStateProvider = appStartupStateController.getAppStartupState() val liveData = startupStateProvider.toLiveData() liveData.observe( @@ -125,9 +140,7 @@ class HomeFragmentPresenter @Inject constructor( if (startUpStateResult.value.startupMode == AppStartupState.StartupMode.USER_NOT_YET_ONBOARDED ) { - analyticsController.logAppOnboardedEvent( - ProfileId.newBuilder().setInternalId(internalProfileId).build() - ) + analyticsController.logAppOnboardedEvent(profileId) } } is AsyncResult.Failure -> { @@ -142,6 +155,43 @@ class HomeFragmentPresenter @Inject constructor( ) } + private fun subscribeToProfileResult(profileId: ProfileId) { + profileManagementController.getProfile(profileId).toLiveData().observe(fragment) { + processProfileResult(it) + } + } + + private fun processProfileResult(result: AsyncResult) { + when (result) { + is AsyncResult.Success -> { + val profile = result.value + handleProfileOnboardingState(profile) + handleBackPress(profile.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 handleProfileOnboardingState(profile: Profile) { + // App onboarding is completed by the first profile on the app(SOLE_LEARNER or SUPERVISOR), + // while profile onboarding is completed by each profile. + if (!profile.completedProfileOnboarding) { + profileManagementController.markProfileOnboardingEnded(profileId) + if (profile.profileType == ProfileType.SOLE_LEARNER || + profile.profileType == ProfileType.SUPERVISOR + ) { + appStartupStateController.markOnboardingFlowCompleted() + logAppOnboardedEvent(profileId) + } + } + } + private fun createRecyclerViewAdapter(): BindableAdapter { return multiTypeBuilderFactory.create { viewModel -> when (viewModel) { @@ -209,4 +259,15 @@ 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) + } + } + ) + } } 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..51891c6ed0c 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,130 @@ 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() { + profileManagementController.getProfiles().toLiveData().observe(activity) { result -> + when (result) { + is AsyncResult.Success -> handleProfiles(result.value) + 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 c7986a03eb5..de53e40ef5b 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 @@ -20,6 +20,7 @@ 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 @@ -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 @@ -174,6 +182,9 @@ class ClassroomListFragmentTest { @Inject lateinit var dataProviderTestMonitor: DataProviderTestMonitor.Factory + @Inject + lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger + private val internalProfileId: Int = 0 private lateinit var profileId: ProfileId @@ -189,9 +200,54 @@ class ClassroomListFragmentTest { @After fun tearDown() { testCoroutineDispatchers.unregisterIdlingResource() + TestPlatformParameterModule.reset() Intents.release() } + @Test + fun testFragment_onLaunch_logsOpenHomeEvent() { + testCoroutineDispatchers.runCurrent() + val event = fakeAnalyticsEventLogger.getOldestEvent() + + assertThat(event.priority).isEqualTo(EventLog.Priority.ESSENTIAL) + assertThat(event.context.activityContextCase).isEqualTo(OPEN_HOME) + } + + @Test + fun testFragment_onFirstLaunch_logsCompleteAppOnboardingEvent() { + TestPlatformParameterModule.forceEnableOnboardingFlowV2(false) + val event = fakeAnalyticsEventLogger.getMostRecentEvent() + + assertThat(event.priority).isEqualTo(EventLog.Priority.OPTIONAL) + assertThat(event.context.activityContextCase).isEqualTo(COMPLETE_APP_ONBOARDING) + } + + @Test + fun testFragment_onboardingV2Enabled_onFirstLaunch_logsCompleteAppOnboardingEvent() { + TestPlatformParameterModule.forceEnableOnboardingFlowV2(true) + val event = fakeAnalyticsEventLogger.getMostRecentEvent() + + assertThat(event.priority).isEqualTo(EventLog.Priority.OPTIONAL) + assertThat(event.context.activityContextCase).isEqualTo(COMPLETE_APP_ONBOARDING) + } + + @Test + fun testFragment_onboardingV2Enabled_onInitialLaunch_logsEndProfileOnboardingEvent() { + TestPlatformParameterModule.forceEnableOnboardingFlowV2(true) + testCoroutineDispatchers.runCurrent() + profileTestHelper.addOnlyAdminProfileWithoutPin() + profileTestHelper.updateProfileType( + profileId = profileId, profileType = ProfileType.SOLE_LEARNER + ) + + // OPEN_HOME, END_PROFILE_ONBOARDING_EVENT and COMPLETE_APP_ONBOARDING are all logged + // concurrently. + val event = fakeAnalyticsEventLogger.getMostRecentEvents(3)[1] + + assertThat(event.priority).isEqualTo(EventLog.Priority.ESSENTIAL) + assertThat(event.context.activityContextCase).isEqualTo(END_PROFILE_ONBOARDING_EVENT) + } + @Test fun testFragment_allComponentsAreDisplayed() { composeRule.onNodeWithTag(WELCOME_TEST_TAG).assertIsDisplayed() @@ -912,6 +968,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 { @@ -925,6 +987,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 b6962460974..8312a3dd7d7 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 @@ -230,7 +231,6 @@ class HomeActivityTest { profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() profileId1 = ProfileId.newBuilder().setInternalId(internalProfileId1).build() testCoroutineDispatchers.registerIdlingResource() - profileTestHelper.initializeProfiles() } @After @@ -264,6 +264,7 @@ class HomeActivityTest { @Test fun testHomeActivity_loadingItemsSuccess_checkProgressbarIsNotDisplayed() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) launch(createHomeActivityIntent(internalProfileId)).use { testCoroutineDispatchers.runCurrent() @@ -289,6 +290,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 { @@ -305,6 +307,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 { @@ -320,6 +323,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 { @@ -335,6 +339,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 { @@ -358,6 +363,7 @@ class HomeActivityTest { @Test fun testPromotedStorySpotlight_setToShowOnSecondLogin_notSeenBefore_checkSpotlightShown() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -368,6 +374,7 @@ class HomeActivityTest { @Test fun testPromotedStoriesSpotlight_setToShowOnSecondLogin_pressDone_checkSpotlightNotShown() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -392,6 +399,7 @@ class HomeActivityTest { @Test fun testHomeActivity_recentlyPlayedStoriesTextIsDisplayed() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -415,6 +423,7 @@ class HomeActivityTest { @Test fun testHomeActivity_viewAllTextIsDisplayed() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -439,6 +448,7 @@ class HomeActivityTest { @Test fun testHomeActivity_storiesPlayedOneWeekAgo_displaysLastPlayedStoriesText() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -463,6 +473,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markStory0DoneForFraction_displaysRecommendedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedFractionsTopic( profileId = profileId1, @@ -495,6 +506,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markCompletedRatiosStory0_recommendsFractions() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedRatiosStory0( profileId = profileId1, @@ -520,6 +532,7 @@ class HomeActivityTest { @Test fun testHomeActivity_noTopicProgress_initialRecommendationFractionsAndRatiosIsCorrect() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -546,6 +559,7 @@ class HomeActivityTest { @Test fun testHomeActivity_forPromotedActivityList_hideViewAll() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -566,6 +580,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markStory0DoneForRatiosAndFirstTestTopic_displaysRecommendedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedTestTopic0Story0( profileId = profileId1, @@ -595,6 +610,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markAtLeastOneStoryCompletedForAllTopics_displaysComingSoonTopicsList() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedFractionsTopic( profileId = profileId1, @@ -632,6 +648,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markFullProgressForSecondTestTopic_displaysComingSoonTopicsText() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedTestTopic1( profileId = profileId1, @@ -674,6 +691,7 @@ class HomeActivityTest { */ @Test fun testHomeActivity_markStory0DonePlayStory1FirstTestTopic_playFractionsTopic_orderIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedTestTopic0Story0( profileId = profileId1, @@ -719,6 +737,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markStory0OfRatiosAndTestTopics0And1Done_playTestTopicStory0_noPromotions() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedRatiosStory0( profileId = profileId1, @@ -756,6 +775,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markStory0DoneFirstTestTopic_recommendedStoriesIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedTestTopic0Story0( profileId = profileId1, @@ -781,6 +801,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markStory0DoneForFrac_recommendedStoriesIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedFractionsStory0( profileId = profileId1, @@ -812,6 +833,7 @@ class HomeActivityTest { @Test fun testHomeActivity_clickViewAll_opensRecentlyPlayedActivity() { + setUpTestWithOnboardingV2Disabled() markSpotlightSeen(profileId1) fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( @@ -843,6 +865,7 @@ class HomeActivityTest { @Test fun testHomeActivity_promotedCard_chapterNameIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -862,6 +885,7 @@ class HomeActivityTest { @Test fun testHomeActivity_promotedCard_storyNameIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -881,6 +905,7 @@ class HomeActivityTest { @Test fun testHomeActivity_configChange_promotedCard_storyNameIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -905,6 +930,7 @@ class HomeActivityTest { @Test fun testHomeActivity_markFullProgressForFractions_playRatios_displaysRecommendedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedRatiosStory0Exp0( profileId = profileId1, @@ -940,6 +966,7 @@ class HomeActivityTest { @Test fun testHomeActivity_clickPromotedStory_opensTopicActivity() { + setUpTestWithOnboardingV2Disabled() markSpotlightSeen(profileId1) fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( @@ -971,6 +998,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( @@ -1003,6 +1031,7 @@ class HomeActivityTest { @Test fun testHomeActivity_promotedCard_topicNameIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -1026,6 +1055,7 @@ class HomeActivityTest { @Test fun testHomeActivity_firstTestTopic_topicSummary_opensTopicActivityThroughPlayIntent() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() markSpotlightSeen(profileId1) launch(createHomeActivityIntent(internalProfileId1)).use { @@ -1051,6 +1081,7 @@ class HomeActivityTest { @Test fun testHomeActivity_firstTestTopic_topicSummary_topicNameIsCorrect() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -1065,6 +1096,7 @@ class HomeActivityTest { @Test fun testHomeActivity_fiveLessons_topicSummary_lessonCountIsCorrect() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -1079,6 +1111,7 @@ class HomeActivityTest { @Test fun testHomeActivity_secondTestTopic_topicSummary_allTopics_topicNameIsCorrect() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedFractionsStory0Exp0( profileId = profileId1, @@ -1098,6 +1131,7 @@ class HomeActivityTest { @Test fun testHomeActivity_oneLesson_topicSummary_lessonCountIsCorrect() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -1115,6 +1149,7 @@ class HomeActivityTest { @Config(qualifiers = "+port-mdpi") @Test fun testHomeActivity_longProfileName_welcomeMessageIsDisplayed() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(longNameInternalProfileId)).use { testCoroutineDispatchers.runCurrent() scrollToPosition(0) @@ -1133,6 +1168,7 @@ class HomeActivityTest { @Config(qualifiers = "+land-mdpi") @Test fun testHomeActivity_configChange_longProfileName_welcomeMessageIsDisplayed() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(longNameInternalProfileId)).use { onView(isRoot()).perform(orientationLandscape()) testCoroutineDispatchers.runCurrent() @@ -1152,6 +1188,7 @@ class HomeActivityTest { @Config(qualifiers = "+sw600dp-port") @Test fun testHomeActivity_longProfileName_tabletPortraitWelcomeMessageIsDisplayed() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(longNameInternalProfileId)).use { testCoroutineDispatchers.runCurrent() scrollToPosition(0) @@ -1170,6 +1207,7 @@ class HomeActivityTest { @Config(qualifiers = "+sw600dp-land") @Test fun testHomeActivity_longProfileName_tabletLandscapeWelcomeMessageIsDisplayed() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(longNameInternalProfileId)).use { onView(isRoot()).perform(orientationLandscape()) testCoroutineDispatchers.runCurrent() @@ -1186,6 +1224,7 @@ class HomeActivityTest { @Test fun testHomeActivity_oneLesson_topicSummary_configChange_lessonCountIsCorrect() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -1201,6 +1240,7 @@ class HomeActivityTest { @Test fun testHomeActivity_clickTopicSummary_opensTopicActivity() { + setUpTestWithOnboardingV2Disabled() logIntoUserTwice() markSpotlightSeen(profileId1) launch(createHomeActivityIntent(internalProfileId1)).use { @@ -1220,6 +1260,7 @@ class HomeActivityTest { @Test fun testHomeActivity_onBackPressed_exitToProfileChooserDialogIsDisplayed() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() pressBack() @@ -1231,6 +1272,7 @@ class HomeActivityTest { @Test fun testHomeActivity_onBackPressed_configChange_exitToProfileChooserDialogIsDisplayed() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() @@ -1244,6 +1286,7 @@ class HomeActivityTest { @Test fun testHomeActivity_onBackPressed_clickExit_opensProfileActivity() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() pressBack() @@ -1256,6 +1299,7 @@ class HomeActivityTest { @Test fun testHomeActivity_checkSpanForItem0_spanSizeIsTwoOrThree() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() if (context.resources.getBoolean(R.bool.isTablet)) { @@ -1268,6 +1312,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)) @@ -1276,6 +1321,7 @@ class HomeActivityTest { @Test fun testHomeActivity_configChange_checkSpanForItem4_spanSizeIsOne() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(internalProfileId1)).use { testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) @@ -1285,6 +1331,7 @@ class HomeActivityTest { @Test fun testHomeActivity_allTopicsCompleted_hidesPromotedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markAllTopicsAsCompleted( profileId = createProfileId(internalProfileId), @@ -1306,6 +1353,7 @@ class HomeActivityTest { @Test fun testHomeActivity_partialProgressForFractionsAndRatios_showsRecentlyPlayedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markCompletedFractionsStory0Exp0( profileId = profileId, @@ -1333,6 +1381,7 @@ class HomeActivityTest { @Test fun testHomeActivity_allTopicsCompleted_displaysAllTopicsHeader() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markAllTopicsAsCompleted( profileId = createProfileId(internalProfileId), @@ -1353,6 +1402,7 @@ class HomeActivityTest { @Config(qualifiers = "+port") @Test fun testHomeActivity_allTopicsCompleted_mobilePortrait_displaysAllTopicCardsIn2Columns() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markAllTopicsAsCompleted( profileId = profileId, @@ -1369,6 +1419,7 @@ class HomeActivityTest { @Config(qualifiers = "+land") @Test fun testHomeActivity_allTopicsCompleted_mobileLandscape_displaysAllTopicCardsIn3Columns() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markAllTopicsAsCompleted( profileId = profileId, @@ -1385,6 +1436,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, @@ -1401,6 +1453,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, @@ -1416,6 +1469,7 @@ class HomeActivityTest { @Test fun testHomeActivity_noTopicsCompleted_displaysAllTopicsHeader() { + setUpTestWithOnboardingV2Disabled() // Only new users will have no progress for any topics. logIntoAdminTwice() launch(createHomeActivityIntent(internalProfileId)).use { @@ -1432,6 +1486,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 { @@ -1451,6 +1506,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 { @@ -1471,6 +1527,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) @@ -1487,6 +1544,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) @@ -1503,6 +1561,7 @@ class HomeActivityTest { @Test fun testHomeActivity_multipleRecentlyPlayedStories_mobileShows3PromotedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedTestTopic0Story0Exp0( profileId = profileId, @@ -1540,6 +1599,7 @@ class HomeActivityTest { @Config(qualifiers = "+sw600dp-port") @Test fun testHomeActivity_multipleRecentlyPlayedStories_tabletPortraitShows3PromotedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedTestTopic0Story0Exp0( profileId = profileId, @@ -1578,6 +1638,7 @@ class HomeActivityTest { @Config(qualifiers = "+sw600dp-land") @Test fun testHomeActivity_multipleRecentlyPlayedStories_tabletLandscapeShows4PromotedStories() { + setUpTestWithOnboardingV2Disabled() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) storyProgressTestHelper.markInProgressSavedTestTopic0Story0Exp0( profileId = profileId, @@ -1615,6 +1676,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) @@ -1643,6 +1705,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 { @@ -1661,6 +1724,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() @@ -1674,6 +1738,7 @@ class HomeActivityTest { @Test @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL]) fun testHomeActivity_defaultState_hasEnglishDisplayLocale() { + setUpTestWithOnboardingV2Disabled() launch(createHomeActivityIntent(internalProfileId)).use { testCoroutineDispatchers.runCurrent() @@ -1688,6 +1753,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 -> @@ -1731,6 +1797,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) @@ -1756,6 +1823,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) @@ -1778,6 +1846,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) @@ -1801,6 +1870,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) @@ -1827,6 +1897,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) @@ -1850,6 +1921,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) @@ -1873,6 +1945,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) @@ -1896,6 +1969,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) @@ -1910,6 +1984,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/CreateProfileFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt index c59489c20c9..ae07a31c261 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt @@ -215,7 +215,8 @@ class CreateProfileFragmentTest { .perform(click()) testCoroutineDispatchers.runCurrent() - val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build() + val expectedParams = + IntroActivityParams.newBuilder().setProfileNickname("John").build() intended( allOf( hasComponent(IntroActivity::class.java.name), @@ -278,7 +279,8 @@ class CreateProfileFragmentTest { .perform(click()) testCoroutineDispatchers.runCurrent() - val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build() + val expectedParams = + IntroActivityParams.newBuilder().setProfileNickname("John").build() intended( allOf( hasComponent(IntroActivity::class.java.name), @@ -325,7 +327,8 @@ class CreateProfileFragmentTest { .perform(click()) testCoroutineDispatchers.runCurrent() - val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build() + val expectedParams = + IntroActivityParams.newBuilder().setProfileNickname("John").build() intended( allOf( hasComponent(IntroActivity::class.java.name), @@ -387,7 +390,8 @@ class CreateProfileFragmentTest { .perform(click()) testCoroutineDispatchers.runCurrent() - val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build() + val expectedParams = + IntroActivityParams.newBuilder().setProfileNickname("John").build() intended( allOf( hasComponent(IntroActivity::class.java.name), 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 c72f4e5721b..96462131914 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 @@ -96,6 +100,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 @@ -116,13 +121,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() } @@ -210,6 +220,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() @@ -219,6 +239,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 fbeb04c4f11..2df546d2caf 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 @@ -100,6 +104,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 @@ -122,19 +127,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() } @@ -336,10 +341,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 8195ac0f683..2c55cdd74fd 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 @@ -320,6 +321,7 @@ class AudioLanguageFragmentTest { @Test fun testFragment_portraitMode_continueButtonClicked_launchesHomeScreen() { + TestPlatformParameterModule.forceEnableMultipleClassrooms(false) initializeTestApplicationComponent(enableOnboardingFlowV2 = true) launch( createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE) @@ -336,6 +338,7 @@ class AudioLanguageFragmentTest { @Test fun testFragment_landscapeMode_continueButtonClicked_launchesHomeScreen() { + TestPlatformParameterModule.forceEnableMultipleClassrooms(false) initializeTestApplicationComponent(enableOnboardingFlowV2 = true) launch( createDefaultAudioActivityIntent(ENGLISH_AUDIO_LANGUAGE) @@ -350,9 +353,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) @@ -382,6 +420,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 5cb9b71ef75..a5cbd3629f2 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 @@ -157,13 +156,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 322f5e2855d..c69652b7ffc 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,6 +45,7 @@ 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.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 @@ -340,6 +341,82 @@ class ProfileChooserFragmentTest { } } + @Test + fun testMigrateProfiles_onboardingV2_clickAdminProfile_checkOpensPinPasswordActivity() { + profileTestHelper.initializeProfiles(autoLogIn = true) + TestPlatformParameterModule.forceEnableOnboardingFlowV2(true) + + 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 = "Learner", + pin = "", + avatarImagePath = null, + allowDownloadAccess = true, + colorRgb = -10710042, + isAdmin = false + ) + 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 testProfileChooserFragment_clickAdminProfileWithNoPin_checkOpensAdminPinActivity() { profileManagementController.addProfile( 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 387ee6650f4..9b577f45949 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 484a9696142..1d466db6d7b 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 @@ -43,7 +43,9 @@ import org.oppia.android.app.application.ApplicationModule import org.oppia.android.app.application.ApplicationStartupListenerModule 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 +53,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 +93,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 +108,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 @@ -122,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 @@ -160,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 @@ -947,7 +957,6 @@ class SplashActivityTest { } @Test - @RunOn(TestPlatform.ROBOLECTRIC) fun testSplashActivity_onboarded_devFlavor_doesNotWaitToStart() { simulateAppAlreadyOnboardedWithFlavor(BuildFlavor.DEVELOPER) initializeTestApplicationWithFlavor(BuildFlavor.DEVELOPER) @@ -1050,6 +1059,72 @@ 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 + @RunOn(TestPlatform.ESPRESSO) + fun testSplashActivity_onboardingV2Enabled_onboardedSoleLearnerProfile_routesToHomeActivity() { + runInNewTestApplication { + profileTestHelper.addOnlyAdminProfileWithoutPin() + appStartupStateController.markOnboardingFlowCompleted() + testCoroutineDispatchers.advanceUntilIdle() + } + initializeTestApplication(onboardingV2Enabled = true) + val profileId = ProfileId.newBuilder().setInternalId(0).build() + profileTestHelper.markProfileOnboardingStarted(profileId) + profileTestHelper.markProfileOnboardingEnded(profileId) + launchSplashActivityPartially { + intended(hasComponent(HomeActivity::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 @@ -1115,8 +1190,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. } @@ -1204,7 +1280,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, @@ -1246,6 +1322,8 @@ class SplashActivityTest { fun getMonitorFactory(): DataProviderTestMonitor.Factory + fun getProfieTestHelper(): ProfileTestHelper + fun inject(splashActivityTest: SplashActivityTest) } @@ -1258,6 +1336,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 3f69bfff0d6..6b66cad60ad 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 @@ -28,6 +28,7 @@ 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.player.state.itemviewmodel.SplitScreenInteractionModule @@ -62,7 +63,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 @@ -71,6 +71,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 @@ -116,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() { @@ -125,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() @@ -142,11 +150,24 @@ class HomeActivityLocalTest { } @Test - fun testHomeActivity_onFirstLaunch_logsCompletedOnboardingEvent() { - setUpTestApplicationComponent() + fun testHomeActivity_onboardingV2_onLaunch_logsOpenHomeEvent() { + setUpTestWithOnboardingV2Enabled(true) + launch(createHomeActivityIntent(profileId)).use { testCoroutineDispatchers.runCurrent() - val event = fakeAnalyticsEventLogger.getMostRecentEvent() + val event = fakeAnalyticsEventLogger.getOldestEvent() + + assertThat(event.priority).isEqualTo(EventLog.Priority.ESSENTIAL) + assertThat(event.context.activityContextCase).isEqualTo(OPEN_HOME) + } + } + + @Test + fun testHomeActivity_onFirstLaunch_logsCompletedAppOnboardingEvent() { + setUpTestWithOnboardingV2Enabled(false) + launch(createHomeActivityIntent(profileId)).use { + testCoroutineDispatchers.runCurrent() + val event = fakeAnalyticsEventLogger.getMostRecentEvents(2).last() assertThat(event.priority).isEqualTo(EventLog.Priority.OPTIONAL) assertThat(event.context.activityContextCase).isEqualTo(COMPLETE_APP_ONBOARDING) @@ -154,13 +175,13 @@ class HomeActivityLocalTest { } @Test - fun testHomeActivity_onSubsequentLaunch_doesNotLogCompletedOnboardingEvent() { + fun testHomeActivity_onSubsequentLaunch_doesNotLogCompletedAppOnboardingEvent() { executeInPreviousAppInstance { testComponent -> testComponent.getAppStartupStateController().markOnboardingFlowCompleted() testComponent.getTestCoroutineDispatchers().runCurrent() } - setUpTestApplicationComponent() + setUpTestWithOnboardingV2Enabled(false) launch(createHomeActivityIntent(profileId)).use { testCoroutineDispatchers.runCurrent() val eventCount = fakeAnalyticsEventLogger.getEventListCount() @@ -172,6 +193,55 @@ class HomeActivityLocalTest { } } + @Test + fun testHomeActivity_onboardingV2Enabled_onInitialLaunch_logsEndProfileOnboardingEvent() { + setUpTestWithOnboardingV2Enabled(true) + profileTestHelper.addOnlyAdminProfileWithoutPin() + launch(createHomeActivityIntent(profileId)).use { + testCoroutineDispatchers.runCurrent() + + // OPEN_HOME, END_PROFILE_ONBOARDING_EVENT and COMPLETE_APP_ONBOARDING are all logged + // concurrently. + val events = fakeAnalyticsEventLogger.getMostRecentEvents(3) + + assertThat(events[1].priority).isEqualTo(EventLog.Priority.OPTIONAL) + assertThat(events[1].context.activityContextCase).isEqualTo(END_PROFILE_ONBOARDING_EVENT) + } + } + + @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 getOneOfLastThreeEventsLogged( + wantedContext: EventLog.Context.ActivityContextCase + ): EventLog { + val events = fakeAnalyticsEventLogger.getMostRecentEvents(3) + return when { + events[0].context.activityContextCase == wantedContext -> events[0] + events[1].context.activityContextCase == wantedContext -> events[1] + else -> events[2] + } + } + + 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. @@ -206,7 +276,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, @@ -244,6 +314,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 ee30f69b061..0da8f5c747a 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 @@ -133,15 +133,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/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt index 287239d6e72..ef99972c99f 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,17 +85,28 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ProfileManagementControllerTest.TestApplication::class) class ProfileManagementControllerTest { - @get:Rule val oppiaTestRule = OppiaTestRule() - @Inject lateinit var context: Context - @Inject lateinit var profileTestHelper: ProfileTestHelper - @Inject lateinit var profileManagementController: ProfileManagementController - @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject lateinit var machineLocale: OppiaLocale.MachineLocale - @field:[BackgroundDispatcher Inject] lateinit var backgroundDispatcher: CoroutineDispatcher - @Inject lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger - @Inject lateinit var loggingIdentifierController: LoggingIdentifierController - @Inject lateinit var oppiaClock: FakeOppiaClock + @get:Rule + val oppiaTestRule = OppiaTestRule() + @Inject + lateinit var context: Context + @Inject + lateinit var profileTestHelper: ProfileTestHelper + @Inject + lateinit var profileManagementController: ProfileManagementController + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject + lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject + lateinit var machineLocale: OppiaLocale.MachineLocale + @field:[BackgroundDispatcher Inject] + lateinit var backgroundDispatcher: CoroutineDispatcher + @Inject + lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger + @Inject + lateinit var loggingIdentifierController: LoggingIdentifierController + @Inject + lateinit var oppiaClock: FakeOppiaClock private companion object { private val PROFILES_LIST = listOf( @@ -122,6 +136,7 @@ class ProfileManagementControllerTest { @After fun tearDown() { TestModule.enableLearnerStudyAnalytics = false + TestModule.enableOnboardingFlowV2 = false } @Test @@ -145,6 +160,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 +1736,126 @@ class ProfileManagementControllerTest { assertThat(failure).hasMessageThat().isEqualTo("ProfileType must be set.") } + @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,6 +2003,11 @@ class ProfileManagementControllerTest { setUpTestApplicationComponent() } + private fun setUpTestWithOnboardingV2Enabled(enableOnboardingV2: Boolean) { + TestModule.enableOnboardingFlowV2 = enableOnboardingV2 + setUpTestApplicationComponent() + } + private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) } @@ -1777,6 +2019,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 +2065,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 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 9bf4d08e951..fd989fe84f4 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -970,6 +970,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..74076d12168 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,21 @@ class ProfileTestHelper @Inject constructor( return monitorFactory.createMonitor(logIntoAdmin()).waitForNextResult() } + /** + * Creates one admin profile without pin and logs in to the profile. + * + * @returns the [AsyncResult] designating the result of attempting to log into the admin profile + */ + fun addOnlyAdminProfileWithoutPin(): AsyncResult { + addProfileAndWait( + name = "Admin", + pin = "", + allowDownloadAccess = true, + isAdmin = true + ) + return monitorFactory.createMonitor(logIntoAdmin()).waitForNextResult() + } + /** Create [numProfiles] number of user profiles. */ fun addMoreProfiles(numProfiles: Int) { for (x in 0 until numProfiles) { @@ -104,6 +120,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/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt index bb7e96e643b..27014a4dd16 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/KenyaAlphaEventTypeToHumanReadableNameConverterImpl.kt @@ -68,6 +68,8 @@ class KenyaAlphaEventTypeToHumanReadableNameConverterImpl @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" } } } diff --git a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt index 315c28c5f3f..337f7908272 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt +++ b/utility/src/main/java/org/oppia/android/util/logging/StandardEventTypeToHumanReadableNameConverterImpl.kt @@ -78,6 +78,8 @@ class StandardEventTypeToHumanReadableNameConverterImpl @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" } } }