Skip to content

Commit 1e8fddd

Browse files
committed
Merge branch 'implement-pin-password' into temp-integration
Conflicts: app/src/main/res/values/colors.xml app/src/main/res/values/styles.xml
2 parents 5a5d00c + 0684cff commit 1e8fddd

File tree

70 files changed

+3555
-154
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+3555
-154
lines changed

app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ dependencies {
7575
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1',
7676
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1',
7777
'org.mockito:mockito-core:2.7.22',
78+
'de.hdodenhof:circleimageview:3.0.1',
79+
'com.chaos.view:pinview:1.4.3'
7880
)
7981
testImplementation(
8082
'androidx.test:core:1.2.0',

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@
1616
android:theme="@style/OppiaThemeWithoutActionBar" />
1717
<activity android:name=".home.HomeActivity" />
1818
<activity android:name=".player.audio.testing.AudioFragmentTestActivity" />
19+
<activity android:name=".profile.AdminAuthActivity" />
20+
<activity android:name=".profile.AddProfileActivity" />
21+
<activity android:name=".profile.PinPasswordActivity"
22+
android:theme="@style/OppiaThemeWithoutActionBar" />
23+
<activity
24+
android:name=".profile.ProfileActivity"
25+
android:theme="@style/OppiaThemeWithoutActionBar"/>
1926
<activity
2027
android:name=".player.exploration.ExplorationActivity"
2128
android:theme="@style/OppiaThemeWithoutActionBar" />
2229
<activity android:name=".player.state.testing.StateFragmentTestActivity" />
23-
<activity android:name=".profile.ProfileActivity" />
2430
<activity
2531
android:name=".splash.SplashActivity"
2632
android:theme="@style/SplashScreenTheme">

app/src/main/java/org/oppia/app/activity/ActivityComponent.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import org.oppia.app.home.continueplaying.ContinuePlayingActivity
99
import org.oppia.app.player.audio.testing.AudioFragmentTestActivity
1010
import org.oppia.app.player.exploration.ExplorationActivity
1111
import org.oppia.app.player.state.testing.StateFragmentTestActivity
12+
import org.oppia.app.profile.AddProfileActivity
13+
import org.oppia.app.profile.AdminAuthActivity
14+
import org.oppia.app.profile.PinPasswordActivity
1215
import org.oppia.app.profile.ProfileActivity
1316
import org.oppia.app.story.StoryActivity
1417
import org.oppia.app.story.testing.StoryFragmentTestActivity
@@ -37,6 +40,8 @@ interface ActivityComponent {
3740

3841
fun getFragmentComponentBuilderProvider(): Provider<FragmentComponent.Builder>
3942

43+
fun inject(addProfileActivity: AddProfileActivity)
44+
fun inject(adminAuthActivity: AdminAuthActivity)
4045
fun inject(audioFragmentTestActivity: AudioFragmentTestActivity)
4146
fun inject(bindableAdapterTestActivity: BindableAdapterTestActivity)
4247
fun inject(conceptCardFragmentTestActivity: ConceptCardFragmentTestActivity)
@@ -45,6 +50,7 @@ interface ActivityComponent {
4550
fun inject(continuePlayingFragmentTestActivity: ContinuePlayingFragmentTestActivity)
4651
fun inject(explorationActivity: ExplorationActivity)
4752
fun inject(homeActivity: HomeActivity)
53+
fun inject(pinPasswordActivity: PinPasswordActivity)
4854
fun inject(htmlParserTestActivity: HtmlParserTestActivity)
4955
fun inject(profileActivity: ProfileActivity)
5056
fun inject(questionPlayerActivity: QuestionPlayerActivity)

app/src/main/java/org/oppia/app/databinding/ImageViewBindingAdapters.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,11 @@ fun setImageDrawable(imageView: ImageView, thumbnailGraphic: LessonThumbnailGrap
4848
}
4949
)
5050
}
51+
52+
@BindingAdapter("profile:src")
53+
fun setProfileImage(imageView: ImageView, imageUrl: String) {
54+
Glide.with(imageView.context)
55+
.load(imageUrl)
56+
.placeholder(R.drawable.default_avatar)
57+
.into(imageView)
58+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.oppia.app.databinding
2+
3+
import android.text.format.DateUtils
4+
import android.widget.ImageView
5+
import android.widget.TextView
6+
import androidx.databinding.BindingAdapter
7+
import java.util.*
8+
9+
/** Converts time in ms to readable relative time string. */
10+
@BindingAdapter("profile:date")
11+
fun setTextWithDate(textView: TextView, timeMs: Long) {
12+
val dateText = "Last used " + DateUtils.getRelativeTimeSpanString(timeMs, Date().time, DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE)
13+
textView.text = dateText
14+
}
15+

app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import org.oppia.app.home.continueplaying.ContinuePlayingFragment
88
import org.oppia.app.player.audio.AudioFragment
99
import org.oppia.app.player.exploration.ExplorationFragment
1010
import org.oppia.app.player.state.StateFragment
11+
import org.oppia.app.profile.AdminSettingsDialogFragment
1112
import org.oppia.app.player.state.itemviewmodel.InteractionViewModelModule
12-
import org.oppia.app.profile.AddProfileFragment
13-
import org.oppia.app.profile.AdminAuthFragment
1413
import org.oppia.app.profile.ProfileChooserFragment
14+
import org.oppia.app.profile.ResetPinDialogFragment
1515
import org.oppia.app.story.StoryFragment
1616
import org.oppia.app.testing.BindableAdapterTestFragment
1717
import org.oppia.app.topic.TopicFragment
@@ -38,8 +38,6 @@ interface FragmentComponent {
3838

3939
fun getViewComponentBuilderProvider(): Provider<ViewComponent.Builder>
4040

41-
fun inject(addProfileFragment: AddProfileFragment)
42-
fun inject(adminAuthFragment: AdminAuthFragment)
4341
fun inject(audioFragment: AudioFragment)
4442
fun inject(bindableAdapterTestFragment: BindableAdapterTestFragment)
4543
fun inject(conceptCardFragment: ConceptCardFragment)
@@ -55,4 +53,6 @@ interface FragmentComponent {
5553
fun inject(topicPlayFragment: TopicPlayFragment)
5654
fun inject(topicReviewFragment: TopicReviewFragment)
5755
fun inject(topicTrainFragment: TopicTrainFragment)
56+
fun inject(adminSettingsDialogFragment: AdminSettingsDialogFragment)
57+
fun inject(resetPinDialogFragment: ResetPinDialogFragment)
5858
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.oppia.app.profile
2+
3+
import android.app.Activity
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import kotlinx.coroutines.ExperimentalCoroutinesApi
7+
import org.oppia.app.activity.InjectableAppCompatActivity
8+
import javax.inject.Inject
9+
10+
/** Fragment that allows users to create new profiles. */
11+
class AddProfileActivity : InjectableAppCompatActivity() {
12+
@Inject lateinit var addProfileFragmentPresenter: AddProfileActivityPresenter
13+
14+
@ExperimentalCoroutinesApi
15+
override fun onCreate(savedInstanceState: Bundle?) {
16+
super.onCreate(savedInstanceState)
17+
activityComponent.inject(this)
18+
addProfileFragmentPresenter.handleOnCreate()
19+
}
20+
21+
override fun onSupportNavigateUp(): Boolean {
22+
finish()
23+
return false
24+
}
25+
26+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
27+
super.onActivityResult(requestCode, resultCode, data)
28+
if (requestCode == GALLERY_INTENT_RESULT_CODE && resultCode == Activity.RESULT_OK) {
29+
addProfileFragmentPresenter.handleOnActivityResult(data)
30+
}
31+
}
32+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package org.oppia.app.profile
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.net.Uri
6+
import android.provider.MediaStore
7+
import android.text.Editable
8+
import android.text.TextWatcher
9+
import android.view.inputmethod.InputMethodManager
10+
import android.widget.ImageView
11+
import androidx.appcompat.app.AlertDialog
12+
import androidx.appcompat.app.AppCompatActivity
13+
import androidx.databinding.DataBindingUtil
14+
import androidx.lifecycle.Observer
15+
import com.bumptech.glide.Glide
16+
import com.bumptech.glide.request.RequestOptions
17+
import kotlinx.coroutines.ExperimentalCoroutinesApi
18+
import org.oppia.app.R
19+
import org.oppia.app.activity.ActivityScope
20+
import org.oppia.app.databinding.AddProfileActivityBinding
21+
import org.oppia.app.viewmodel.ViewModelProvider
22+
import org.oppia.domain.profile.ProfileManagementController
23+
import javax.inject.Inject
24+
25+
const val GALLERY_INTENT_RESULT_CODE = 1
26+
27+
/** The presenter for [AddProfileActivity]. */
28+
@ActivityScope
29+
class AddProfileActivityPresenter @Inject constructor(
30+
private val activity: AppCompatActivity,
31+
private val profileManagementController: ProfileManagementController,
32+
private val viewModelProvider: ViewModelProvider<AddProfileViewModel>
33+
) {
34+
lateinit var uploadImageView: ImageView
35+
private val addViewModel by lazy {
36+
getAddProfileViewModel()
37+
}
38+
private var selectedImage: Uri? = null
39+
var allowDownloadAccess = false
40+
var inputtedPin = false
41+
var inputtedConfirmPin = false
42+
43+
@ExperimentalCoroutinesApi
44+
fun handleOnCreate() {
45+
activity.title = "Add Profile"
46+
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
47+
activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
48+
49+
val binding = DataBindingUtil.setContentView<AddProfileActivityBinding>(activity, R.layout.add_profile_activity)
50+
51+
binding.apply {
52+
viewModel = addViewModel
53+
}
54+
55+
binding.allowDownloadSwitch.setOnCheckedChangeListener { _, isChecked ->
56+
allowDownloadAccess = isChecked
57+
}
58+
59+
binding.infoIcon.setOnClickListener {
60+
showInfoDialog()
61+
}
62+
63+
addTextChangeListeners(binding)
64+
65+
binding.uploadImageButton.setOnClickListener {
66+
val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
67+
activity.startActivityForResult(galleryIntent, GALLERY_INTENT_RESULT_CODE)
68+
}
69+
uploadImageView = binding.uploadImageButton
70+
71+
binding.createButton.setOnClickListener {
72+
addViewModel.clearAllErrorMessages()
73+
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
74+
imm?.hideSoftInputFromWindow(activity.currentFocus?.windowToken, 0)
75+
val name = binding.inputName.getInput()
76+
val pin = binding.inputPin.getInput()
77+
val confirmPin = binding.inputConfirmPin.getInput()
78+
var failed = false
79+
if (name.isEmpty()) {
80+
addViewModel.nameErrorMsg.set(activity.resources.getString(R.string.add_profile_error_name_empty))
81+
failed = true
82+
}
83+
if (pin.isNotEmpty() && pin.length < 3) {
84+
addViewModel.pinErrorMsg.set(activity.resources.getString(R.string.add_profile_error_pin_length))
85+
failed = true
86+
}
87+
if (pin != confirmPin) {
88+
addViewModel.confirmPinErrorMsg.set(activity.resources.getString(R.string.add_profile_error_pin_confirm_wrong))
89+
failed = true
90+
}
91+
if (failed) {
92+
binding.scroll.smoothScrollTo(0,0)
93+
return@setOnClickListener
94+
}
95+
profileManagementController.addProfile(name, pin, selectedImage, allowDownloadAccess, isAdmin = false).observe(activity, Observer {
96+
if (it.isSuccess()) {
97+
val intent = Intent(activity, ProfileActivity::class.java)
98+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
99+
activity.startActivity(intent)
100+
} else if (it.isFailure()) {
101+
when (it.getErrorOrNull()) {
102+
is ProfileManagementController.ProfileNameNotUniqueException -> addViewModel.nameErrorMsg.set(activity.resources.getString(R.string.add_profile_error_name_not_unique))
103+
is ProfileManagementController.ProfileNameOnlyLettersException -> addViewModel.nameErrorMsg.set(activity.resources.getString(R.string.add_profile_error_name_only_letters))
104+
}
105+
binding.scroll.smoothScrollTo(0,0)
106+
}
107+
})
108+
}
109+
}
110+
111+
fun handleOnActivityResult(data: Intent?) {
112+
data?.let {
113+
selectedImage = data.data
114+
Glide.with(activity)
115+
.load(selectedImage)
116+
.centerCrop()
117+
.apply(RequestOptions.circleCropTransform())
118+
.into(uploadImageView)
119+
}
120+
}
121+
122+
private fun addTextChangeListeners(binding: AddProfileActivityBinding) {
123+
fun setValidPin() {
124+
if (inputtedPin && inputtedConfirmPin) {
125+
addViewModel.validPin.set(true)
126+
} else {
127+
binding.allowDownloadSwitch.isChecked = false
128+
addViewModel.validPin.set(false)
129+
}
130+
}
131+
132+
binding.inputPin.addTextChangedListener(object: TextWatcher {
133+
override fun onTextChanged(pin: CharSequence?, start: Int, before: Int, count: Int) {
134+
pin?.let {
135+
addViewModel.pinErrorMsg.set("")
136+
inputtedPin = it.isNotEmpty()
137+
setValidPin()
138+
}
139+
}
140+
override fun afterTextChanged(confirmPin: Editable?) {}
141+
override fun beforeTextChanged(p0: CharSequence?, start: Int, count: Int, after: Int) {}
142+
})
143+
144+
binding.inputConfirmPin.addTextChangedListener(object: TextWatcher {
145+
override fun onTextChanged(confirmPin: CharSequence?, start: Int, before: Int, count: Int) {
146+
confirmPin?.let {
147+
addViewModel.confirmPinErrorMsg.set("")
148+
inputtedConfirmPin = confirmPin.isNotEmpty()
149+
setValidPin()
150+
}
151+
}
152+
override fun afterTextChanged(confirmPin: Editable?) {}
153+
override fun beforeTextChanged(p0: CharSequence?, start: Int, count: Int, after: Int) {}
154+
})
155+
156+
binding.inputName.addTextChangedListener(object: TextWatcher {
157+
override fun onTextChanged(confirmPin: CharSequence?, start: Int, before: Int, count: Int) {
158+
confirmPin?.let {
159+
addViewModel.nameErrorMsg.set("")
160+
}
161+
}
162+
override fun afterTextChanged(confirmPin: Editable?) {}
163+
override fun beforeTextChanged(p0: CharSequence?, start: Int, count: Int, after: Int) {}
164+
})
165+
}
166+
167+
private fun showInfoDialog() {
168+
AlertDialog.Builder(activity as Context, R.style.AlertDialogTheme)
169+
.setMessage(R.string.add_profile_pin_info)
170+
.setPositiveButton(R.string.add_profile_close) { dialog, _ ->
171+
dialog.dismiss()
172+
}.create().show()
173+
}
174+
175+
private fun getAddProfileViewModel(): AddProfileViewModel {
176+
return viewModelProvider.getForActivity(activity, AddProfileViewModel::class.java)
177+
}
178+
}

app/src/main/java/org/oppia/app/profile/AddProfileFragment.kt

Lines changed: 0 additions & 23 deletions
This file was deleted.

app/src/main/java/org/oppia/app/profile/AddProfileFragmentPresenter.kt

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)