Skip to content

Commit

Permalink
Fix Part of #4938: Introduce Create profile screen (#5380)
Browse files Browse the repository at this point in the history
<!-- READ ME FIRST: Please fill in the explanation section below and
check off every point from the Essential Checklist! -->
## Explanation
Fix Part of #4938: Add a new Activity and associated Fragments and
Presenters to allow a new learner to create a profile.
This does not include domain changes. 

- Learner should be able to click “Continue” if they have entered their
nickname, even without adding a profile picture.
- Learner should not be able to click “Continue” if they have not
entered their nickname and an error message should be displayed.
- The learner can select a profile picture.

Placeholder tests have been added to ensure navigation tests are not
forgotten. These will fail once navigation has been implemented.

## Essential Checklist
<!-- Please tick the relevant boxes by putting an "x" in them. -->
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).

## For UI-specific PRs only
||||
| --- | --- | --- |
|| Portrait | Landscape |
|Mobile Light
Mode|![Screenshot_1719774012](https://github.com/oppia/oppia-android/assets/59600948/189f7e62-8761-4d38-b859-e73df23d1221)|![Screenshot_1719774028](https://github.com/oppia/oppia-android/assets/59600948/9e8b01c5-4b60-40aa-94ae-da4093241759)|
|Tablet Dark
Mode|![Screenshot_1719774236](https://github.com/oppia/oppia-android/assets/59600948/e5e161f9-77e7-4b1d-a2bc-b7b0646b71c2)|![Screenshot_1719774244](https://github.com/oppia/oppia-android/assets/59600948/97820f77-a628-48b0-a325-f929118594bc)|

## All Tests Passing on Espresso
![Screenshot 2024-05-24 at 01 42
41](https://github.com/oppia/oppia-android/assets/59600948/9e301e33-d11f-48a5-9f55-fca3e155b00e)
  • Loading branch information
adhiamboperes committed Jun 30, 2024
1 parent b278b5e commit df47253
Show file tree
Hide file tree
Showing 32 changed files with 1,823 additions and 12 deletions.
1 change: 1 addition & 0 deletions app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/home/recentlyplayed/PromotedStoryViewModel.kt",
"src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedViewModel.kt",
"src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt",
"src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.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/ongoingtopiclist/OngoingTopicItemViewModel.kt",
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,10 @@
android:name=".app.onboarding.OnboardingProfileTypeActivity"
android:label="@string/onboarding_profile_type_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />

<activity
android:name=".app.onboarding.CreateProfileActivity"
android:label="@string/create_profile_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import org.oppia.android.app.help.thirdparty.ThirdPartyDependencyListActivity
import org.oppia.android.app.home.HomeActivity
import org.oppia.android.app.home.recentlyplayed.RecentlyPlayedActivity
import org.oppia.android.app.mydownloads.MyDownloadsActivity
import org.oppia.android.app.onboarding.CreateProfileActivity
import org.oppia.android.app.onboarding.OnboardingActivity
import org.oppia.android.app.onboarding.OnboardingProfileTypeActivity
import org.oppia.android.app.ongoingtopiclist.OngoingTopicListActivity
Expand Down Expand Up @@ -222,4 +223,5 @@ interface ActivityComponentImpl :
fun inject(colorBindingAdaptersTestActivity: ColorBindingAdaptersTestActivity)
fun inject(classroomListActivity: ClassroomListActivity)
fun inject(onboardingProfileTypeActivity: OnboardingProfileTypeActivity)
fun inject(createProfileActivity: CreateProfileActivity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ 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.CreateProfileFragment
import org.oppia.android.app.onboarding.OnboardingFragment
import org.oppia.android.app.onboarding.OnboardingProfileTypeFragment
import org.oppia.android.app.ongoingtopiclist.OngoingTopicListFragment
Expand Down Expand Up @@ -198,4 +199,5 @@ interface FragmentComponentImpl : FragmentComponent, ViewComponentBuilderInjecto
fun inject(surveyOutroDialogFragment: SurveyOutroDialogFragment)
fun inject(classroomListFragment: ClassroomListFragment)
fun inject(onboardingProfileTypeFragment: OnboardingProfileTypeFragment)
fun inject(createProfileFragment: CreateProfileFragment)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.oppia.android.app.onboarding

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.CREATE_PROFILE_ACTIVITY
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 createProfileActivityIntent(context: Context): Intent {
return Intent(context, CreateProfileActivity::class.java).apply {
decorateWithScreenName(CREATE_PROFILE_ACTIVITY)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.oppia.android.app.onboarding

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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.oppia.android.app.onboarding

import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
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 createProfileFragmentPresenter: CreateProfileFragmentPresenter

override fun onAttach(context: Context) {
super.onAttach(context)
(fragmentComponent as FragmentComponentImpl).inject(this)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
createProfileFragmentPresenter.activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
createProfileFragmentPresenter.handleOnActivityResult(result.data)
}
}
return createProfileFragmentPresenter.handleCreateView(inflater, container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.oppia.android.app.onboarding

import android.content.Intent
import android.graphics.PorterDuff
import android.provider.MediaStore
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import org.oppia.android.R
import org.oppia.android.app.fragment.FragmentScope
import org.oppia.android.databinding.CreateProfileFragmentBinding
import org.oppia.android.util.parser.image.ImageLoader
import org.oppia.android.util.parser.image.ImageViewTarget
import javax.inject.Inject

/** Presenter for [CreateProfileFragment]. */
@FragmentScope
class CreateProfileFragmentPresenter @Inject constructor(
private val fragment: Fragment,
private val activity: AppCompatActivity,
private val createProfileViewModel: CreateProfileViewModel,
private val imageLoader: ImageLoader
) {
private lateinit var binding: CreateProfileFragmentBinding
private lateinit var uploadImageView: ImageView
private lateinit var selectedImage: String

/** Launcher for picking an image from device gallery. */
lateinit var activityResultLauncher: ActivityResultLauncher<Intent>

/** Initialize layout bindings. */
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View {
binding = CreateProfileFragmentBinding.inflate(
inflater,
container,
/* attachToRoot= */ false
)
binding.let {
it.lifecycleOwner = fragment
it.viewModel = createProfileViewModel
}

uploadImageView = binding.createProfileUserImageView

uploadImageView.apply {
setColorFilter(
ResourcesCompat.getColor(
activity.resources,
R.color.component_color_avatar_background_25_color,
null
),
PorterDuff.Mode.DST_OVER
)

imageLoader.loadDrawable(
R.drawable.ic_profile_icon,
ImageViewTarget(this)
)
}

binding.onboardingNavigationContinue.setOnClickListener {
val nickname = binding.createProfileNicknameEdittext.text.toString().trim()

createProfileViewModel.hasErrorMessage.set(nickname.isBlank())
}

binding.createProfileNicknameEdittext.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable?) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
createProfileViewModel.hasErrorMessage.set(false)
}
})

addViewOnClickListeners(binding)

return binding.root
}

/** Receive the result of image upload and load it into the image view. */
fun handleOnActivityResult(intent: Intent?) {
intent?.let {
binding.createProfilePicturePrompt.visibility = View.GONE
selectedImage =
checkNotNull(intent.data.toString()) { "Could not find the selected image." }
imageLoader.loadBitmap(
selectedImage,
ImageViewTarget(uploadImageView)
)
}
}

private fun addViewOnClickListeners(binding: CreateProfileFragmentBinding) {
val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)

binding.onboardingNavigationBack.setOnClickListener { activity.finish() }
binding.createProfileEditPictureIcon.setOnClickListener {
activityResultLauncher.launch(
galleryIntent
)
}
binding.createProfilePicturePrompt.setOnClickListener {
activityResultLauncher.launch(
galleryIntent
)
}
binding.createProfileUserImageView.setOnClickListener {
activityResultLauncher.launch(
galleryIntent
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.oppia.android.app.onboarding

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 CreateProfileViewModel @Inject constructor() : ObservableViewModel() {

/** ObservableField that tracks whether creating a nickname has triggered an error condition. */
val hasErrorMessage = ObservableField(false)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
binding.apply {
lifecycleOwner = fragment

profileTypeLearnerNavigationCard.setOnClickListener {
val intent = CreateProfileActivity.createProfileActivityIntent(activity)
fragment.startActivity(intent)
}

profileTypeSupervisorNavigationCard.setOnClickListener {
val intent = ProfileChooserActivity.createProfileChooserActivity(activity)
fragment.startActivity(intent)
Expand All @@ -36,6 +41,7 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
activity.finish()
}
}

return binding.root
}
}
33 changes: 33 additions & 0 deletions app/src/main/res/drawable/create_profile_picture_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<corners android:radius="10dp" />
<size
android:width="48dp"
android:height="48dp" />
<gradient
android:endColor="@android:color/transparent"
android:gradientRadius="60"
android:startColor="@color/component_color_onboarding_shared_black_color"
android:type="radial" />
</shape>
</item>
<item
android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp">
<shape android:shape="oval">
<solid android:color="@color/component_color_onboarding_profile_edit_icon_color" />
<corners android:radius="10dp" />
</shape>
</item>
<item
android:bottom="16dp"
android:drawable="@drawable/ic_outline_edit_24"
android:left="16dp"
android:right="16dp"
android:state_enabled="true"
android:top="16dp" />
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="@color/component_color_shared_white_background_color" />
<stroke
android:width="1dp"
android:color="@color/component_color_shared_error_color" />
</shape>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="@color/component_color_shared_white_background_color" />
<stroke
android:width="1dp"
android:color="@color/component_color_edittext_stroke_color" />
</shape>
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_outline_edit_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M14.06,9.02l0.92,0.92L5.92,19L5,19v-0.92l9.06,-9.06M17.66,3c-0.25,0 -0.51,0.1 -0.7,0.29l-1.83,1.83 3.75,3.75 1.83,-1.83c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.2,-0.2 -0.45,-0.29 -0.71,-0.29zM14.06,6.19L3,17.25L3,21h3.75L17.81,9.94l-3.75,-3.75z"/>
</vector>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_profile_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M48,48H0v-0.52A21.65,21.65 0,0 1,0.47 42.6a4.62,4.62 0,0 1,3.71 -3.49c4.05,-1 8.08,-2 12.12,-3.08a0.55,0.55 0,0 0,0.5 -0.62c0,-1 0,-2.09 0,-3.14a1,1 0,0 0,-0.18 -0.56,14.57 14.57,0 0,1 -3.21,-6.06c-0.07,-0.26 -0.2,-0.29 -0.47,-0.29a2,2 0,0 1,-1.12 -0.25,5 5,0 0,1 -1.19,-1.19c-1.11,-1.55 -1,-3.31 -0.77,-5.06a1.7,1.7 0,0 1,1.28 -1.54,0.4 0.4,0 0,0 0.33,-0.52A45,45 0,0 1,11.09 8a3.68,3.68 0,0 1,0.67 -2,10.11 10.11,0 0,1 0.71,-0.88 10.76,10.76 0,0 1,2.9 -2.19,0.86 0.86,0 0,0 0.2,-0.18 1.09,1.09 0,0 0,-0.25 0l-0.71,0.1a1.69,1.69 0,0 1,-0.23 0c0,-0.06 0.09,-0.15 0.16,-0.18 0.79,-0.35 1.57,-0.71 2.37,-1A9.17,9.17 0,0 1,22.54 0.85a0.66,0.66 0,0 0,0.24 0l0.53,-0.14L22.86,0.5 22.63,0.36A1.1,1.1 0,0 1,22.89 0.3c0.95,-0.1 1.9,-0.21 2.85,-0.26a4.44,4.44 0,0 1,1.79 0.1,7.77 7.77,0 0,1 3.89,3.12 0.68,0.68 0,0 0,0.56 0.26,4.37 4.37,0 0,1 3,0.55 1.66,1.66 0,0 1,0.62 0.67c0.31,0.7 0.55,1.43 0.81,2.15a1.09,1.09 0,0 1,0.06 0.36,3.07 3.07,0 0,0 -0.31,-0.17L35.9,7a1.29,1.29 0,0 0,0.05 0.35,2.08 2.08,0 0,0 0.32,0.4 2.33,2.33 0,0 1,0.66 1.75c-0.13,2.4 -0.24,4.8 -0.38,7.2 0,0.36 0,0.58 0.4,0.69a1.59,1.59 0,0 1,1.19 1.45,16.27 16.27,0 0,1 0,3.12 4.29,4.29 0,0 1,-1.66 3,2.57 2.57,0 0,1 -1.33,0.49c-0.4,0 -0.56,0.1 -0.65,0.47a14.44,14.44 0,0 1,-3 5.73,1 1,0 0,0 -0.25,0.63c0,1.07 0,2.15 0,3.23a0.54,0.54 0,0 0,0.48 0.59c4,1 7.91,2.08 11.89,3 2.25,0.53 3.67,1.76 4,4C47.86,44.66 47.88,46.34 48,48Z"
android:fillColor="#FFFFFF"/>
</vector>
Loading

0 comments on commit df47253

Please sign in to comment.