diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 48ab86b20b1..2e155aeb696 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -214,6 +214,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt", "src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt", "src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt", + "src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateLearnerProfileViewModel.kt", "src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt", "src/main/java/org/oppia/android/app/options/OptionsReadingTextSizeViewModel.kt", "src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt", @@ -409,9 +410,9 @@ VIEWS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/customview/ChapterNotStartedContainerConstraintLayout.kt", "src/main/java/org/oppia/android/app/customview/ContinueButtonView.kt", "src/main/java/org/oppia/android/app/customview/LessonThumbnailImageView.kt", + "src/main/java/org/oppia/android/app/customview/OppiaCurveBackgroundView.kt", "src/main/java/org/oppia/android/app/customview/PromotedStoryCardView.kt", "src/main/java/org/oppia/android/app/customview/SegmentedCircularProgressView.kt", - "src/main/java/org/oppia/android/app/customview/SurveyOnboardingBackgroundView.kt", "src/main/java/org/oppia/android/app/customview/VerticalDashedLineView.kt", "src/main/java/org/oppia/android/app/survey/SurveyMultipleChoiceOptionView.kt", "src/main/java/org/oppia/android/app/survey/SurveyNpsItemOptionView.kt", diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 41d1ce55918..1dee1b80db5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,7 @@ + + android:theme="@style/OppiaThemeWithoutActionBar" /> + + + + override fun onAttach(context: Context) { super.onAttach(context) (fragmentComponent as FragmentComponentImpl).inject(this) @@ -24,6 +34,10 @@ class OnboardingFragment : InjectableFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - return onboardingFragmentPresenter.handleCreateView(inflater, container) + return if (enableOnboardingFlowV2.value) { + onboardingFragmentPresenterV2.handleCreateView(inflater, container) + } else { + onboardingFragmentPresenter.handleCreateView(inflater, container) + } } } diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/AudioLanguageFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/AudioLanguageFragmentPresenter.kt new file mode 100644 index 00000000000..bf934d926fe --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/AudioLanguageFragmentPresenter.kt @@ -0,0 +1,123 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.res.Configuration +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +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.home.HomeActivity +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.ProfileId +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.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import javax.inject.Inject + +/** The presenter for [AudioLanguageFragment] V2. */ +class AudioLanguageFragmentPresenter @Inject constructor( + private val fragment: Fragment, + private val activity: AppCompatActivity, + private val appLanguageResourceHandler: AppLanguageResourceHandler, + private val profileManagementController: ProfileManagementController, + private val oppiaLogger: OppiaLogger +) { + private lateinit var binding: AudioLanguageSelectionFragmentBinding + private val orientation = Resources.getSystem().configuration.orientation + + /** + * Returns a newly inflated view to render the fragment with the specified [audioLanguage] as the + * initial selected language. + */ + fun handleCreateView( + inflater: LayoutInflater, + container: ViewGroup? + ): View { + + activity.findViewById(R.id.reading_list_app_bar_layout).visibility = View.GONE + + binding = AudioLanguageSelectionFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + binding.lifecycleOwner = fragment + + binding.audioLanguageText.text = appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.audio_language_fragment_text, + appLanguageResourceHandler.getStringInLocale(R.string.app_name) + ) + + binding.audioLanguageDropdown.adapter = ArrayAdapter( + fragment.requireContext(), + R.layout.onboarding_language_dropdown_item, + R.id.onboarding_language_text_view, + getAudioLanguageList() + ) + + val currentUserProfileId = retrieveNewProfileId() + + binding.onboardingNavigationContinue.setOnClickListener { + val intent = + HomeActivity.createHomeActivity(fragment.requireContext(), currentUserProfileId.internalId) + fragment.startActivity(intent) + fragment.activity?.finish() + } + + binding.onboardingNavigationBack.setOnClickListener { + activity.finish() + } + + binding.onboardingStepsCount?.visibility = + if (orientation == Configuration.ORIENTATION_PORTRAIT) View.VISIBLE else View.GONE + + return binding.root + } + + private fun retrieveNewProfileId(): ProfileId { + var profileId: ProfileId = ProfileId.getDefaultInstance() + profileManagementController.getProfiles().toLiveData().observe( + fragment, + { profilesResult -> + when (profilesResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "AudioLanguageFragmentPresenter", + "Failed to retrieve the list of profiles", + profilesResult.error + ) + } + is AsyncResult.Pending -> {} + is AsyncResult.Success -> { + val sortedProfileList = profilesResult.value.sortedBy { it.id.internalId } + profileId = sortedProfileList.last().id + } + } + } + ) + return profileId + } + + private fun getAudioLanguageList(): List { + return AudioLanguage.values() + .filter { it.isValid() } + .map { audioLanguage -> + appLanguageResourceHandler.computeLocalizedDisplayName(audioLanguage) + } + } + + private fun AudioLanguage.isValid(): Boolean { + return when (this) { + AudioLanguage.UNRECOGNIZED, AudioLanguage.AUDIO_LANGUAGE_UNSPECIFIED, + AudioLanguage.NO_AUDIO -> false + else -> true + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateLearnerProfileViewModel.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateLearnerProfileViewModel.kt new file mode 100644 index 00000000000..c3ae8b184a3 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateLearnerProfileViewModel.kt @@ -0,0 +1,21 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.res.Configuration +import android.content.res.Resources +import android.view.View +import androidx.databinding.ObservableField +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.viewmodel.ObservableViewModel +import javax.inject.Inject + +/** The ViewModel for [CreateProfileFragment]. */ +@FragmentScope +class CreateLearnerProfileViewModel @Inject constructor() : ObservableViewModel() { + private val orientation = Resources.getSystem().configuration.orientation + + /** ObservableField that tracks whether a nickname has been entered. */ + val hasError = ObservableField(false) + + val onboardingStepsCount = + if (orientation == Configuration.ORIENTATION_PORTRAIT) View.VISIBLE else View.GONE +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileActivity.kt new file mode 100644 index 00000000000..52e1dfa8729 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileActivity.kt @@ -0,0 +1,32 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity +import org.oppia.android.app.model.ScreenName +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName +import javax.inject.Inject + +/** Activity for displaying a new learner profile creation flow. */ +class CreateProfileActivity : InjectableAutoLocalizedAppCompatActivity() { + @Inject + lateinit var learnerProfileActivityPresenter: CreateProfileActivityPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + learnerProfileActivityPresenter.handleOnCreate() + } + + companion object { + /** Returns a new [Intent] open a [CreateProfileActivity] with the specified params. */ + fun createNewLearnerProfileActivity(context: Context): Intent { + return Intent(context, CreateProfileActivity::class.java).apply { + decorateWithScreenName(ScreenName.CREATE_NEW_LEARNER_PROFILE_ACTIVITY) + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileActivityPresenter.kt new file mode 100644 index 00000000000..2727e648415 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileActivityPresenter.kt @@ -0,0 +1,40 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import org.oppia.android.R +import org.oppia.android.databinding.CreateProfileActivityBinding +import javax.inject.Inject + +private const val TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT = "TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT" + +/** Presenter for [CreateProfileActivity]. */ +class CreateProfileActivityPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + private lateinit var binding: CreateProfileActivityBinding + + /** Handle creation and binding of the CreateProfileActivity layout. */ + fun handleOnCreate() { + binding = DataBindingUtil.setContentView(activity, R.layout.create_profile_activity) + binding.apply { + lifecycleOwner = activity + } + + if (getNewLearnerProfileFragment() == null) { + val createLearnerProfileFragment = CreateProfileFragment() + activity.supportFragmentManager.beginTransaction().add( + R.id.profile_fragment_placeholder, + createLearnerProfileFragment, + TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT + ) + .commitNow() + } + } + + private fun getNewLearnerProfileFragment(): CreateProfileFragment? { + return activity.supportFragmentManager.findFragmentByTag( + TAG_CREATE_PROFILE_ACTIVITY_FRAGMENT + ) as? CreateProfileFragment + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileFragment.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileFragment.kt new file mode 100644 index 00000000000..6ad586d2f46 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileFragment.kt @@ -0,0 +1,35 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import javax.inject.Inject + +/** Fragment for displaying a new learner profile creation flow. */ +class CreateProfileFragment : InjectableFragment() { + @Inject + lateinit var learnerProfileFragmentPresenter: CreateProfileFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return learnerProfileFragmentPresenter.handleCreateView(inflater, container) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + learnerProfileFragmentPresenter.handleOnActivityResult(requestCode, resultCode, data) + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileFragmentPresenter.kt new file mode 100644 index 00000000000..e139bf63d5f --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/CreateProfileFragmentPresenter.kt @@ -0,0 +1,170 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.app.Activity +import android.content.Intent +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.net.Uri +import android.provider.MediaStore +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.Fragment +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import org.oppia.android.R +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.databinding.CreateProfileFragmentBinding +import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import javax.inject.Inject + +const val GALLERY_INTENT_RESULT_CODE = 1 + +/** Presenter for [CreateProfileFragment]. */ +@FragmentScope +class CreateProfileFragmentPresenter @Inject constructor( + private val fragment: Fragment, + private val activity: AppCompatActivity, + private val createLearnerProfileViewModel: CreateLearnerProfileViewModel, + private val profileManagementController: ProfileManagementController, + private val resourceHandler: AppLanguageResourceHandler +) { + private lateinit var binding: CreateProfileFragmentBinding + private lateinit var uploadImageView: ImageView + private var selectedImage: Uri? = null + + /** Initialize layout bindings. */ + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View { + binding = CreateProfileFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + binding.let { + it.lifecycleOwner = fragment + it.viewModel = createLearnerProfileViewModel + } + + uploadImageView = binding.createProfileUserImageView + Glide.with(activity) + .load(R.drawable.ic_default_avatar) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + uploadImageView.setColorFilter( + ResourcesCompat.getColor( + activity.resources, + R.color.component_color_avatar_background_25_color, + null + ), + PorterDuff.Mode.DST_OVER + ) + return false + } + }) + .into(uploadImageView) + + binding.onboardingNavigationContinue.setOnClickListener { + val nickname = binding.createProfileNicknameEdittext.text.toString().trim() + + if (nickname.isNotBlank()) { + createLearnerProfileViewModel.hasError.set(false) + profileManagementController.addProfile( + name = nickname, + pin = "", + avatarImagePath = selectedImage, + allowDownloadAccess = true, + colorRgb = -10710042, + isAdmin = false + ) + .toLiveData() + .observe( + fragment, + { + handleAddProfileResult(it, nickname) + } + ) + } else { + createLearnerProfileViewModel.hasError.set(true) + } + } + + binding.onboardingNavigationBack.setOnClickListener { activity.finish() } + binding.createProfileEditPictureIcon.setOnClickListener { openGalleryIntent() } + binding.createProfilePicturePrompt.setOnClickListener { openGalleryIntent() } + binding.createProfileUserImageView.setOnClickListener { openGalleryIntent() } + + return binding.root + } + + fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == GALLERY_INTENT_RESULT_CODE && resultCode == Activity.RESULT_OK) { + binding.createProfilePicturePrompt.visibility = View.GONE + data?.let { + selectedImage = data.data + Glide.with(activity) + .load(selectedImage) + .centerCrop() + .apply(RequestOptions.circleCropTransform()) + .into(uploadImageView) + } + } + } + + private fun openGalleryIntent() { + val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + fragment.startActivityForResult(galleryIntent, GALLERY_INTENT_RESULT_CODE) + } + + private fun handleAddProfileResult(result: AsyncResult, nickName: String) { + when (result) { + is AsyncResult.Success -> { + val intent = + OnboardingLearnerIntroActivity.createOnboardingLearnerIntroActivity(activity, nickName) + fragment.startActivity(intent) + } + is AsyncResult.Failure -> { + createLearnerProfileViewModel.hasError.set(true) + when (result.error) { + is ProfileManagementController.ProfileNameNotUniqueException -> + binding.createProfileNicknameError.text = + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_not_unique + ) + + is ProfileManagementController.ProfileNameOnlyLettersException -> + binding.createProfileNicknameError.text = + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_only_letters + ) + else -> binding.createProfileNicknameError.text = result.error.message + } + } + is AsyncResult.Pending -> {} // Wait for an actual result. + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingFragmentPresenter.kt new file mode 100644 index 00000000000..26671f02a1e --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingFragmentPresenter.kt @@ -0,0 +1,47 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import org.oppia.android.R +import org.oppia.android.app.fragment.FragmentScope +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.databinding.OnboardingAppLanguageSelectionFragmentBinding +import javax.inject.Inject + +/** The presenter for [OnboardingFragment] V2. */ +@FragmentScope +class OnboardingFragmentPresenter @Inject constructor( + private val activity: AppCompatActivity, + private val fragment: Fragment, + private val appLanguageResourceHandler: AppLanguageResourceHandler +) { + private lateinit var binding: OnboardingAppLanguageSelectionFragmentBinding + + /** Handle creation and binding of the [OnboardingFragment] V2 layout. */ + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View { + binding = OnboardingAppLanguageSelectionFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + + binding.let { + it.lifecycleOwner = fragment + } + + binding.onboardingLanguageTitle.text = appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.onboarding_language_activity_title, + appLanguageResourceHandler.getStringInLocale(R.string.app_name) + ) + + binding.onboardingLanguageLetsGoButton.setOnClickListener { + val intent = OnboardingProfileTypeActivity.createOnboardingProfileTypeActivityIntent(activity) + fragment.startActivity(intent) + } + + return binding.root + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroActivity.kt new file mode 100644 index 00000000000..d8ab2e417fa --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroActivity.kt @@ -0,0 +1,59 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity +import org.oppia.android.app.model.OnboardingIntroActivityParams +import org.oppia.android.app.model.ScreenName +import org.oppia.android.util.extensions.getProtoExtra +import org.oppia.android.util.extensions.putProtoExtra +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName +import javax.inject.Inject + +/** The activity for showing the learner welcome screen. */ +class OnboardingLearnerIntroActivity : InjectableAutoLocalizedAppCompatActivity() { + @Inject + lateinit var onboardingLearnerIntroActivityPresenter: OnboardingLearnerIntroActivityPresenter + + private lateinit var profileNickname: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + val params = intent.extractParams() + this.profileNickname = params.profileNickname + + onboardingLearnerIntroActivityPresenter.handleOnCreate(profileNickname) + } + + companion object { + private const val PARAMS_KEY = "OnboardingIntroActivity.params" + + /** + * A convenience function for creating a new [OnboardingLearnerIntroActivity] intent by prefilling + * common params needed by the activity. + */ + fun createOnboardingLearnerIntroActivity(context: Context, profileNickname: String): Intent { + val params = OnboardingIntroActivityParams.newBuilder() + .setProfileNickname(profileNickname) + .build() + return createOnboardingLearnerIntroActivity(context, params) + } + + private fun createOnboardingLearnerIntroActivity( + context: Context, + params: OnboardingIntroActivityParams + ): Intent { + return Intent(context, OnboardingLearnerIntroActivity::class.java).apply { + putProtoExtra(PARAMS_KEY, params) + decorateWithScreenName(ScreenName.ONBOARDING_LEARNER_INTRO_ACTIVITY) + } + } + + private fun Intent.extractParams() = + getProtoExtra(PARAMS_KEY, OnboardingIntroActivityParams.getDefaultInstance()) + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroActivityPresenter.kt new file mode 100644 index 00000000000..fab8ad9cda8 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroActivityPresenter.kt @@ -0,0 +1,51 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.databinding.OnboardingLearnerIntroActivityBinding +import javax.inject.Inject + +private const val TAG_LEARNER_INTRO_FRAGMENT = "TAG_LEARNER_INTRO_FRAGMENT" + +/** Argument key for bundling the profileId. */ +const val PROFILE_NICKNAME_ARGUMENT_KEY = "profile_nickname" + +/** The Presenter for [OnboardingLearnerIntroActivity]. */ +@ActivityScope +class OnboardingLearnerIntroActivityPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + private lateinit var binding: OnboardingLearnerIntroActivityBinding + + /** Handle creation and binding of the OnboardingLearnerIntroActivity layout. */ + fun handleOnCreate(profileNickname: String) { + binding = DataBindingUtil.setContentView(activity, R.layout.onboarding_learner_intro_activity) + binding.apply { + lifecycleOwner = activity + } + + if (getOnboardingLearnerIntroFragment() == null) { + val onboardingLearnerIntroFragment = OnboardingLearnerIntroFragment() + + val args = Bundle() + args.putString(PROFILE_NICKNAME_ARGUMENT_KEY, profileNickname) + onboardingLearnerIntroFragment.arguments = args + + activity.supportFragmentManager.beginTransaction().add( + R.id.learner_intro_fragment_placeholder, + onboardingLearnerIntroFragment, + TAG_LEARNER_INTRO_FRAGMENT + ) + .commitNow() + } + } + + private fun getOnboardingLearnerIntroFragment(): OnboardingLearnerIntroFragment? { + return activity.supportFragmentManager.findFragmentByTag( + TAG_LEARNER_INTRO_FRAGMENT + ) as? OnboardingLearnerIntroFragment + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroFragment.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroFragment.kt new file mode 100644 index 00000000000..fd6aea347db --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroFragment.kt @@ -0,0 +1,35 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import org.oppia.android.util.extensions.getStringFromBundle +import javax.inject.Inject + +/** Fragment that contains the introduction message for new learners. */ +class OnboardingLearnerIntroFragment : InjectableFragment() { + @Inject + lateinit var onboardingLearnerIntroFragmentPresenter: OnboardingLearnerIntroFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val profileNickname = arguments!!.getStringFromBundle(PROFILE_NICKNAME_ARGUMENT_KEY)!! + return onboardingLearnerIntroFragmentPresenter.handleCreateView( + inflater, + container, + profileNickname + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroFragmentPresenter.kt new file mode 100644 index 00000000000..ba9c2791d8a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingLearnerIntroFragmentPresenter.kt @@ -0,0 +1,72 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.res.Configuration +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import org.oppia.android.R +import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.options.AudioLanguageActivity +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.databinding.OnboardingLearnerIntroFragmentBinding +import javax.inject.Inject + +/** The presenter for [OnboardingLearnerIntroFragment]. */ +class OnboardingLearnerIntroFragmentPresenter @Inject constructor( + private var fragment: Fragment, + private val activity: AppCompatActivity, + private val appLanguageResourceHandler: AppLanguageResourceHandler, +) { + private lateinit var binding: OnboardingLearnerIntroFragmentBinding + + private val orientation = Resources.getSystem().configuration.orientation + + /** Handle creation and binding of the OnboardingLearnerIntroFragment layout. */ + fun handleCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + profileNickname: String, + ): View { + binding = OnboardingLearnerIntroFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + binding.lifecycleOwner = fragment + + setLearnerName(profileNickname) + + binding.onboardingNavigationBack.setOnClickListener { + activity.finish() + } + + binding.onboardingNavigationContinue.setOnClickListener { + val intent = AudioLanguageActivity.createAudioLanguageActivityIntent( + fragment.requireContext(), + AudioLanguage.ENGLISH_AUDIO_LANGUAGE + ) + fragment.startActivity(intent) + } + + binding.onboardingLearnerIntroFeedback.text = + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.onboarding_learner_intro_feedback_text, + appLanguageResourceHandler.getStringInLocale(R.string.app_name) + ) + + binding.onboardingStepsCount.visibility = + if (orientation == Configuration.ORIENTATION_PORTRAIT) View.VISIBLE else View.GONE + + return binding.root + } + + private fun setLearnerName(profileName: String) { + binding.onboardingLearnerIntroTitle.text = + appLanguageResourceHandler.getStringInLocaleWithWrapping( + R.string.onboarding_learner_intro_activity_text, profileName + ) + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeActivity.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeActivity.kt new file mode 100644 index 00000000000..bea2bc36c83 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeActivity.kt @@ -0,0 +1,32 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import org.oppia.android.app.activity.ActivityComponentImpl +import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity +import org.oppia.android.app.model.ScreenName +import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.decorateWithScreenName +import javax.inject.Inject + +/** The activity for showing the profile type selection screen. */ +class OnboardingProfileTypeActivity : InjectableAutoLocalizedAppCompatActivity() { + @Inject + lateinit var onboardingProfileTypeActivityPresenter: OnboardingProfileTypeActivityPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (activityComponent as ActivityComponentImpl).inject(this) + + onboardingProfileTypeActivityPresenter.handleOnCreate() + } + + companion object { + /** Returns a new [Intent] open a [OnboardingProfileTypeActivity] with the specified params. */ + fun createOnboardingProfileTypeActivityIntent(context: Context): Intent { + return Intent(context, OnboardingProfileTypeActivity::class.java).apply { + decorateWithScreenName(ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY) + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeActivityPresenter.kt new file mode 100644 index 00000000000..f678ed058c3 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeActivityPresenter.kt @@ -0,0 +1,42 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.databinding.OnboardingProfileTypeActivityBinding +import javax.inject.Inject + +private const val TAG_PROFILE_TYPE_FRAGMENT = "TAG_PROFILE_TYPE_FRAGMENT" + +/** The Presenter for [OnboardingProfileTypeActivity]. */ +@ActivityScope +class OnboardingProfileTypeActivityPresenter @Inject constructor( + private val activity: AppCompatActivity +) { + private lateinit var binding: OnboardingProfileTypeActivityBinding + + /** Handle creation and binding of the OnboardingProfileTypeActivity layout. */ + fun handleOnCreate() { + binding = DataBindingUtil.setContentView(activity, R.layout.onboarding_profile_type_activity) + binding.apply { + lifecycleOwner = activity + } + + if (getOnboardingProfileTypeFragment() == null) { + val onboardingProfileTypeFragment = OnboardingProfileTypeFragment() + activity.supportFragmentManager.beginTransaction().add( + R.id.profile_type_fragment_placeholder, + onboardingProfileTypeFragment, + TAG_PROFILE_TYPE_FRAGMENT + ) + .commitNow() + } + } + + private fun getOnboardingProfileTypeFragment(): OnboardingProfileTypeFragment? { + return activity.supportFragmentManager.findFragmentByTag( + TAG_PROFILE_TYPE_FRAGMENT + ) as? OnboardingProfileTypeFragment + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeFragment.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeFragment.kt new file mode 100644 index 00000000000..21bf3750217 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeFragment.kt @@ -0,0 +1,29 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.oppia.android.app.fragment.FragmentComponentImpl +import org.oppia.android.app.fragment.InjectableFragment +import javax.inject.Inject + +/** Fragment that contains the profile type selection flow of the app. */ +class OnboardingProfileTypeFragment : InjectableFragment() { + @Inject + lateinit var onboardingProfileTypeFragmentPresenter: OnboardingProfileTypeFragmentPresenter + + override fun onAttach(context: Context) { + super.onAttach(context) + (fragmentComponent as FragmentComponentImpl).inject(this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return onboardingProfileTypeFragmentPresenter.handleCreateView(inflater, container) + } +} diff --git a/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeFragmentPresenter.kt new file mode 100644 index 00000000000..47f5242909a --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/onboarding/onboardingv2/OnboardingProfileTypeFragmentPresenter.kt @@ -0,0 +1,45 @@ +package org.oppia.android.app.onboarding.onboardingv2 + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import org.oppia.android.app.profile.ProfileChooserActivity +import org.oppia.android.databinding.OnboardingProfileTypeFragmentBinding +import javax.inject.Inject + +/** The presenter for [OnboardingProfileTypeFragment]. */ +class OnboardingProfileTypeFragmentPresenter @Inject constructor( + private val fragment: Fragment, + private val activity: AppCompatActivity +) { + private lateinit var binding: OnboardingProfileTypeFragmentBinding + + /** Handle creation and binding of the OnboardingProfileTypeFragment layout. */ + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View { + binding = OnboardingProfileTypeFragmentBinding.inflate( + inflater, + container, + /* attachToRoot= */ false + ) + binding.let { + it.lifecycleOwner = fragment + } + + binding.profileTypeLearnerNavigationCard.setOnClickListener { + val intent = CreateProfileActivity.createNewLearnerProfileActivity(activity) + fragment.startActivity(intent) + } + + binding.profileTypeSupervisorNavigationCard.setOnClickListener { + val intent = ProfileChooserActivity.createProfileChooserActivity(activity) + fragment.startActivity(intent) + } + + binding.onboardingNavigationBack.setOnClickListener { + activity.finish() + } + return binding.root + } +} diff --git a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt index fd98e6259cd..95621e93602 100644 --- a/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt +++ b/app/src/main/java/org/oppia/android/app/options/AudioLanguageFragment.kt @@ -12,11 +12,22 @@ import org.oppia.android.app.model.AudioLanguageFragmentArguments import org.oppia.android.app.model.AudioLanguageFragmentStateBundle import org.oppia.android.util.extensions.getProto import org.oppia.android.util.extensions.putProto +import org.oppia.android.util.platformparameter.EnableOnboardingFlowV2 +import org.oppia.android.util.platformparameter.PlatformParameterValue import javax.inject.Inject +import org.oppia.android.app.onboarding.onboardingv2.AudioLanguageFragmentPresenter as AudioLanguageFragmentPresenterV2 /** The fragment to change the default audio language of the app. */ class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonListener { - @Inject lateinit var audioLanguageFragmentPresenter: AudioLanguageFragmentPresenter + @Inject + lateinit var audioLanguageFragmentPresenter: AudioLanguageFragmentPresenter + + @Inject + lateinit var audioLanguageFragmentPresenterV2: AudioLanguageFragmentPresenterV2 + + @Inject + @field:EnableOnboardingFlowV2 + lateinit var enableOnboardingFlowV2: PlatformParameterValue override fun onAttach(context: Context) { super.onAttach(context) @@ -33,7 +44,11 @@ class AudioLanguageFragment : InjectableFragment(), AudioLanguageRadioButtonList savedInstanceState?.retrieveLanguageFromSavedState() ?: arguments?.retrieveLanguageFromArguments() ) { "Expected arguments to be passed to AudioLanguageFragment" } - return audioLanguageFragmentPresenter.handleOnCreateView(inflater, container, audioLanguage) + return if (enableOnboardingFlowV2.value) { + audioLanguageFragmentPresenterV2.handleCreateView(inflater, container) + } else { + audioLanguageFragmentPresenter.handleOnCreateView(inflater, container, audioLanguage) + } } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/org/oppia/android/app/survey/SurveyActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/survey/SurveyActivityPresenter.kt index 8020dfe8796..c0b7847dfe7 100644 --- a/app/src/main/java/org/oppia/android/app/survey/SurveyActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/survey/SurveyActivityPresenter.kt @@ -11,8 +11,13 @@ import javax.inject.Inject private const val TAG_SURVEY_FRAGMENT = "TAG_SURVEY_FRAGMENT" +/** Argument key for bundling the profileId. */ const val PROFILE_ID_ARGUMENT_KEY = "profile_id" + +/** Argument key for bundling the topicId. */ const val TOPIC_ID_ARGUMENT_KEY = "topic_id" + +/** Argument key for bundling the explorationId. */ const val EXPLORATION_ID_ARGUMENT_KEY = "exploration_id" /** The Presenter for [SurveyActivity]. */ @@ -20,6 +25,7 @@ const val EXPLORATION_ID_ARGUMENT_KEY = "exploration_id" class SurveyActivityPresenter @Inject constructor(private val activity: AppCompatActivity) { private lateinit var binding: SurveyActivityBinding + /** Handle creation and binding of the SurveyActivity layout. */ fun handleOnCreate( profileId: ProfileId, topicId: String, diff --git a/app/src/main/java/org/oppia/android/app/view/ViewComponentImpl.kt b/app/src/main/java/org/oppia/android/app/view/ViewComponentImpl.kt index 08726cc7f49..ecb07ff3e28 100644 --- a/app/src/main/java/org/oppia/android/app/view/ViewComponentImpl.kt +++ b/app/src/main/java/org/oppia/android/app/view/ViewComponentImpl.kt @@ -6,9 +6,9 @@ import dagger.Subcomponent import org.oppia.android.app.customview.ChapterNotStartedContainerConstraintLayout import org.oppia.android.app.customview.ContinueButtonView import org.oppia.android.app.customview.LessonThumbnailImageView +import org.oppia.android.app.customview.OppiaCurveBackgroundView import org.oppia.android.app.customview.PromotedStoryCardView import org.oppia.android.app.customview.SegmentedCircularProgressView -import org.oppia.android.app.customview.SurveyOnboardingBackgroundView import org.oppia.android.app.home.promotedlist.ComingSoonTopicsListView import org.oppia.android.app.home.promotedlist.PromotedStoryListView import org.oppia.android.app.player.state.DragDropSortInteractionView @@ -42,7 +42,7 @@ interface ViewComponentImpl : ViewComponent { fun inject(promotedStoryCardView: PromotedStoryCardView) fun inject(promotedStoryListView: PromotedStoryListView) fun inject(segmentedCircularProgressView: SegmentedCircularProgressView) - fun inject(surveyOnboardingBackgroundView: SurveyOnboardingBackgroundView) + fun inject(curveBackgroundView: OppiaCurveBackgroundView) fun inject(surveyMultipleChoiceOptionView: SurveyMultipleChoiceOptionView) fun inject(surveyNpsItemOptionView: SurveyNpsItemOptionView) } diff --git a/app/src/main/res/drawable/create_profile_picture_icon.xml b/app/src/main/res/drawable/create_profile_picture_icon.xml new file mode 100644 index 00000000000..487cfa6f97f --- /dev/null +++ b/app/src/main/res/drawable/create_profile_picture_icon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/dropdown_background.xml b/app/src/main/res/drawable/dropdown_background.xml new file mode 100644 index 00000000000..3eca06cfe8c --- /dev/null +++ b/app/src/main/res/drawable/dropdown_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/edit_text_error_border.xml b/app/src/main/res/drawable/edit_text_error_border.xml new file mode 100644 index 00000000000..6a42f9c1956 --- /dev/null +++ b/app/src/main/res/drawable/edit_text_error_border.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/edit_text_white_background_error_border.xml b/app/src/main/res/drawable/edit_text_white_background_error_border.xml new file mode 100644 index 00000000000..2851e1fc4cb --- /dev/null +++ b/app/src/main/res/drawable/edit_text_white_background_error_border.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/edit_text_white_background_with_border.xml b/app/src/main/res/drawable/edit_text_white_background_with_border.xml new file mode 100644 index 00000000000..90e111c7c1a --- /dev/null +++ b/app/src/main/res/drawable/edit_text_white_background_with_border.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_green_check.xml b/app/src/main/res/drawable/ic_green_check.xml new file mode 100644 index 00000000000..67bb482c387 --- /dev/null +++ b/app/src/main/res/drawable/ic_green_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_language_icon_black_24dp.xml b/app/src/main/res/drawable/ic_language_icon_black_24dp.xml new file mode 100644 index 00000000000..e89dd7a2a58 --- /dev/null +++ b/app/src/main/res/drawable/ic_language_icon_black_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_edit_24.xml b/app/src/main/res/drawable/ic_outline_edit_24.xml new file mode 100644 index 00000000000..cd18181145e --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_edit_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/learner_otter.png b/app/src/main/res/drawable/learner_otter.png new file mode 100644 index 00000000000..7d492965cda Binary files /dev/null and b/app/src/main/res/drawable/learner_otter.png differ diff --git a/app/src/main/res/drawable/onboarding_back_button_white_background.xml b/app/src/main/res/drawable/onboarding_back_button_white_background.xml new file mode 100644 index 00000000000..47b0bf7daa1 --- /dev/null +++ b/app/src/main/res/drawable/onboarding_back_button_white_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/otter.png b/app/src/main/res/drawable/otter.png new file mode 100644 index 00000000000..9f42e7f8dcc Binary files /dev/null and b/app/src/main/res/drawable/otter.png differ diff --git a/app/src/main/res/drawable/parent_teacher_otter.png b/app/src/main/res/drawable/parent_teacher_otter.png new file mode 100644 index 00000000000..ec2c4a546c7 Binary files /dev/null and b/app/src/main/res/drawable/parent_teacher_otter.png differ diff --git a/app/src/main/res/drawable/rounded_primary_button_grey_shadow_color.xml b/app/src/main/res/drawable/rounded_primary_button_grey_shadow_color.xml index 1566be1839b..b9b38dc514d 100644 --- a/app/src/main/res/drawable/rounded_primary_button_grey_shadow_color.xml +++ b/app/src/main/res/drawable/rounded_primary_button_grey_shadow_color.xml @@ -7,9 +7,9 @@ - + - + diff --git a/app/src/main/res/layout-land/audio_language_selection_fragment.xml b/app/src/main/res/layout-land/audio_language_selection_fragment.xml new file mode 100644 index 00000000000..fb0dcb54f48 --- /dev/null +++ b/app/src/main/res/layout-land/audio_language_selection_fragment.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + +