Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #4938: Revised Profile Configuration 3 of 3 #5328

Closed
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.oppia.android.app.splash

import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
Expand All @@ -9,12 +10,15 @@ 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.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.Profile
import org.oppia.android.app.model.ProfileOnboardingState
import org.oppia.android.app.notice.AutomaticAppDeprecationNoticeDialogFragment
import org.oppia.android.app.notice.BetaNoticeDialogFragment
import org.oppia.android.app.notice.DeprecationNoticeActionResponse
Expand All @@ -31,6 +35,7 @@ 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.topic.PrimeTopicAssetsController
import org.oppia.android.domain.translation.TranslationController
import org.oppia.android.util.data.AsyncResult
Expand All @@ -51,6 +56,7 @@ private const val OS_UPDATE_NOTICE_DIALOG_FRAGMENT_TAG = "os_update_notice_dialo
private const val SPLASH_INIT_STATE_DATA_PROVIDER_ID = "splash_init_state_data_provider"

/** The presenter for [SplashActivity]. */
@SuppressLint("CustomSplashScreen")
@ActivityScope
class SplashActivityPresenter @Inject constructor(
private val activity: AppCompatActivity,
Expand All @@ -65,6 +71,7 @@ class SplashActivityPresenter @Inject constructor(
private val currentBuildFlavor: BuildFlavor,
@EnableAppAndOsDeprecation
private val enableAppAndOsDeprecation: PlatformParameterValue<Boolean>,
private val profileManagementController: ProfileManagementController
) {
lateinit var startupMode: StartupMode

Expand Down Expand Up @@ -290,6 +297,9 @@ class SplashActivityPresenter @Inject constructor(
AutomaticAppDeprecationNoticeDialogFragment::newInstance
)
}
StartupMode.ONBOARDING_FLOW_V2 -> {
getProfileOnboardingState()
}
else -> {
// In all other cases (including errors when the startup state fails to load or is
// defaulted), assume the user needs to be onboarded.
Expand All @@ -299,6 +309,64 @@ class SplashActivityPresenter @Inject constructor(
}
}

private fun getProfileOnboardingState() {
appStartupStateController.getProfileOnboardingState().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(onboardingState: ProfileOnboardingState) {
when (onboardingState) {
ProfileOnboardingState.NEW_INSTALL -> {
activity.startActivity(OnboardingActivity.createOnboardingActivity(activity))
activity.finish()
}
ProfileOnboardingState.SOLE_LEARNER_PROFILE -> {
profileManagementController.getProfiles().toLiveData().observe(
activity,
{ result ->
when (result) {
is AsyncResult.Success -> {
val internalProfileId = getSoleLearnerProfile(result.value)?.id?.internalId
activity.startActivity(HomeActivity.createHomeActivity(activity, internalProfileId))
activity.finish()
}
is AsyncResult.Pending -> {} // no-op
is AsyncResult.Failure -> {
oppiaLogger.e(
"SplashActivity", "Failed to retrieve the list of profiles", result.error
)
}
}
}
)
}
else -> {
activity.startActivity(ProfileChooserActivity.createProfileChooserActivity(activity))
activity.finish()
}
}
}

private fun getSoleLearnerProfile(profiles: List<Profile>): Profile? {
return profiles.find { it.isAdmin }
}

private fun computeInitStateDataProvider(): DataProvider<SplashInitState> {
val startupStateDataProvider = appStartupStateController.getAppStartupState()
val systemAppLanguageLocaleDataProvider = translationController.getSystemLanguageLocale()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ 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.DeprecationResponseDatabase
import org.oppia.android.app.model.OnboardingState
import org.oppia.android.app.model.ProfileOnboardingState
import org.oppia.android.data.persistence.PersistentCacheStore
import org.oppia.android.domain.oppialogger.OppiaLogger
import org.oppia.android.domain.profile.ProfileManagementController
import org.oppia.android.util.data.DataProvider
import org.oppia.android.util.data.DataProviders.Companion.combineWith
import org.oppia.android.util.data.DataProviders.Companion.transform
import org.oppia.android.util.extensions.getStringFromBundle
import org.oppia.android.util.locale.OppiaLocale
import org.oppia.android.util.platformparameter.EnableAppAndOsDeprecation
import org.oppia.android.util.platformparameter.PlatformParameterValue
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton

private const val APP_STARTUP_STATE_PROVIDER_ID = "app_startup_state_data_provider_id"
private const val PROFILE_ONBOARDING_STATE_PROVIDER_ID = "profile_onboarding_state_data_provider_id"

/** Controller for persisting and retrieving the user's initial app state upon opening the app. */
@Singleton
Expand All @@ -29,8 +29,7 @@ class AppStartupStateController @Inject constructor(
private val machineLocale: OppiaLocale.MachineLocale,
private val currentBuildFlavor: BuildFlavor,
private val deprecationController: DeprecationController,
@EnableAppAndOsDeprecation
private val enableAppAndOsDeprecation: Provider<PlatformParameterValue<Boolean>>,
private val profileManagementController: ProfileManagementController
) {
private val onboardingFlowStore by lazy {
cacheStoreFactory.create("on_boarding_flow", OnboardingState.getDefaultInstance())
Expand Down Expand Up @@ -104,7 +103,10 @@ class AppStartupStateController @Inject constructor(
APP_STARTUP_STATE_PROVIDER_ID
) { onboardingState, deprecationResponseDatabase ->
AppStartupState.newBuilder().apply {
startupMode = computeAppStartupMode(onboardingState, deprecationResponseDatabase)
startupMode = deprecationController.processStartUpMode(
onboardingState,
deprecationResponseDatabase
)
buildFlavorNoticeMode = computeBuildNoticeMode(onboardingState, startupMode)
}.build()
}
Expand All @@ -127,23 +129,6 @@ class AppStartupStateController @Inject constructor(
}
}

private fun computeAppStartupMode(
onboardingState: OnboardingState,
deprecationResponseDatabase: DeprecationResponseDatabase
): 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 when {
hasAppExpired() -> StartupMode.APP_IS_DEPRECATED
onboardingState.alreadyOnboardedApp -> StartupMode.USER_IS_ONBOARDED
else -> StartupMode.USER_NOT_YET_ONBOARDED
}
}

return deprecationController.processStartUpMode(onboardingState, deprecationResponseDatabase)
}

private fun computeBuildNoticeMode(
onboardingState: OnboardingState,
startupMode: StartupMode
Expand Down Expand Up @@ -190,4 +175,26 @@ class AppStartupStateController @Inject constructor(
expirationDate?.isBeforeToday() ?: true
} else false
}

/** Returns the state of the app based on the number and type of existing profiles. */
fun getProfileOnboardingState(): DataProvider<ProfileOnboardingState> {
return profileManagementController.getProfiles()
.transform(PROFILE_ONBOARDING_STATE_PROVIDER_ID) { profileList ->
when {
profileList.size > 1 -> {
ProfileOnboardingState.MULTIPLE_PROFILES
}
profileList.size == 1 -> {
if (profileList.first().isAdmin && profileList.first().pin.isNotBlank()) {
ProfileOnboardingState.ADMIN_PROFILE_ONLY
} else {
ProfileOnboardingState.SOLE_LEARNER_PROFILE
}
}
else -> {
ProfileOnboardingState.NEW_INSTALL
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.oppia.android.util.data.DataProvider
import org.oppia.android.util.data.DataProviders
import org.oppia.android.util.data.DataProviders.Companion.transform
import org.oppia.android.util.extensions.getVersionCode
import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2
import org.oppia.android.util.platformparameter.ForcedAppUpdateVersionCode
import org.oppia.android.util.platformparameter.LowestSupportedApiLevel
import org.oppia.android.util.platformparameter.OptionalAppUpdateVersionCode
Expand All @@ -41,7 +42,9 @@ class DeprecationController @Inject constructor(
@ForcedAppUpdateVersionCode
private val forcedAppUpdateVersionCode: Provider<PlatformParameterValue<Int>>,
@LowestSupportedApiLevel
private val lowestSupportedApiLevel: Provider<PlatformParameterValue<Int>>
private val lowestSupportedApiLevel: Provider<PlatformParameterValue<Int>>,
@EnableOnboardingFlowV2
private val enableOnboardingFlowV2: PlatformParameterValue<Boolean>
) {
/** Create an instance of [PersistentCacheStore] that contains a [DeprecationResponseDatabase]. */
private val deprecationStore by lazy {
Expand Down Expand Up @@ -173,6 +176,10 @@ class DeprecationController @Inject constructor(
return StartupMode.OPTIONAL_UPDATE_AVAILABLE
}

if (enableOnboardingFlowV2.value) {
return StartupMode.ONBOARDING_FLOW_V2
}

return StartupMode.USER_IS_ONBOARDED
} else return StartupMode.USER_NOT_YET_ONBOARDED
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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.ProfileType
import org.oppia.android.app.model.ReadingTextSize
import org.oppia.android.data.persistence.PersistentCacheStore
import org.oppia.android.data.persistence.PersistentCacheStore.PublishMode
Expand All @@ -30,6 +31,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
Expand Down Expand Up @@ -89,7 +91,9 @@ class ProfileManagementController @Inject constructor(
private val enableLearnerStudyAnalytics: PlatformParameterValue<Boolean>,
@EnableLoggingLearnerStudyIds
private val enableLoggingLearnerStudyIds: PlatformParameterValue<Boolean>,
private val profileNameValidator: ProfileNameValidator
private val profileNameValidator: ProfileNameValidator,
@EnableOnboardingFlowV2
private val enableOnboardingFlowV2: PlatformParameterValue<Boolean>
) {
private var currentProfileId: Int = DEFAULT_LOGGED_OUT_INTERNAL_PROFILE_ID
private val profileDataStore =
Expand Down Expand Up @@ -290,6 +294,10 @@ class ProfileManagementController @Inject constructor(
avatarImageUri = imageUri
} else avatarColorRgb = colorRgb
}.build()

if (enableOnboardingFlowV2.value) {
this.profileType = computeProfileType(it)
}
}.build()

val wasProfileEverAdded = it.profilesCount > 0
Expand All @@ -306,6 +314,27 @@ class ProfileManagementController @Inject constructor(
}
}

private fun computeProfileType(profileDatabase: ProfileDatabase): ProfileType {
return if (isAdminWithPin(profileDatabase)) {
ProfileType.SUPERVISOR
} else {
if (profileDatabase.profilesCount == 1) {
ProfileType.SOLE_LEARNER
} else {
ProfileType.ADDITIONAL_LEARNER
}
}
}

private fun isAdminWithPin(profileDatabase: ProfileDatabase): Boolean {
profileDatabase.profilesMap.values.forEach {
if (it.isAdmin && it.hasPin) {
return true
}
}
return false
}

/**
* Updates the profile avatar of an existing profile.
*
Expand Down Expand Up @@ -684,7 +713,7 @@ class ProfileManagementController @Inject constructor(
return@createInMemoryDataProviderAsync AsyncResult.Success(0)
}
AsyncResult.Failure(
ProfileNotFoundException(
ProfileManagementController.ProfileNotFoundException(
"ProfileId ${profileId.internalId} is" +
" not associated with an existing profile"
)
Expand Down
23 changes: 23 additions & 0 deletions model/src/main/proto/onboarding.proto
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ message AppStartupState {
// they are using an OS version that is no longer supported. The user should be shown a prompt
// to update their OS.
OS_IS_DEPRECATED = 5;

// Indicates that the onboarding flow shown to the user should be the new flow.
// TODO(#): Remove after onboarding project stabilization.
ONBOARDING_FLOW_V2 = 6;
}

// Describes different notices that may be shown to the user on startup depending on whether
Expand Down Expand Up @@ -69,6 +73,7 @@ message AppStartupState {

// Stores the completion state of the user's progress through the app onboarding flow.
message OnboardingState {
// TODO(#): Remove after onboarding project stabilization
// Indicates whether user has fully completed the onboarding flow.
bool already_onboarded_app = 1;

Expand All @@ -83,3 +88,21 @@ message OnboardingState {
// the general availability version of the app after having previously used a pre-release version.
bool permanently_dismissed_ga_upgrade_notice = 4;
}

// Indicates the state of the app with regards to the number and type of existing profiles.
enum ProfileOnboardingState {
// Indicates that the number or type of profiles is unknown.
PROFILE_ONBOARDING_STATE_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 = 2;

// Indicates that there is only one profile and it is an admin profile.
ADMIN_PROFILE_ONLY = 3;

// Indicates that there are multiple profiles on the device.
MULTIPLE_PROFILES = 4;
}
17 changes: 17 additions & 0 deletions model/src/main/proto/oppia_logger.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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
Expand Down Expand Up @@ -178,6 +183,18 @@ message EventLog {
}
}

// 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.
Expand Down
Loading
Loading