Improve password protection

This commit is contained in:
Koitharu
2021-04-10 16:34:44 +03:00
parent 0f48ad07a3
commit 012416c881
20 changed files with 412 additions and 108 deletions

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.text.InputType
@@ -17,6 +18,7 @@ import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.utils.ext.*
import java.io.File
@@ -77,6 +79,10 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
?: getString(R.string.not_available)
}
}
AppSettings.KEY_APP_PASSWORD -> {
findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty()
}
}
}
@@ -102,8 +108,10 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
true
}
AppSettings.KEY_PROTECT_APP -> {
if ((preference as? SwitchPreference ?: return false).isChecked) {
enableAppProtection(preference)
val pref = (preference as? SwitchPreference ?: return false)
if (pref.isChecked) {
pref.isChecked = false
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
} else {
settings.appPassword = null
}

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.backup.RestoreRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
val settingsModule
get() = module {
@@ -19,4 +20,5 @@ val settingsModule
viewModel { BackupViewModel(get(), androidContext()) }
viewModel { (uri: Uri?) -> RestoreViewModel(uri, get(), androidContext()) }
viewModel { ProtectSetupViewModel(get()) }
}

View File

@@ -0,0 +1,97 @@
package org.koitharu.kotatsu.settings.protect
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.graphics.Insets
import androidx.core.view.isGone
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivitySetupProtectBinding
class ProtectSetupActivity : BaseActivity<ActivitySetupProtectBinding>(), TextWatcher,
View.OnClickListener, TextView.OnEditorActionListener {
private val viewModel by viewModel<ProtectSetupViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySetupProtectBinding.inflate(layoutInflater))
binding.editPassword.addTextChangedListener(this)
binding.editPassword.setOnEditorActionListener(this)
binding.buttonNext.setOnClickListener(this)
binding.buttonCancel.setOnClickListener(this)
viewModel.isSecondStep.observe(this, this::onStepChanged)
viewModel.onPasswordSet.observe(this) {
finishAfterTransition()
}
viewModel.onPasswordMismatch.observe(this) {
binding.editPassword.error = getString(R.string.passwords_mismatch)
}
viewModel.onClearText.observe(this) {
binding.editPassword.text?.clear()
}
}
override fun onWindowInsetsChanged(insets: Insets) {
val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
binding.root.setPadding(
basePadding + insets.left,
basePadding + insets.top,
basePadding + insets.right,
basePadding + insets.bottom
)
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> finish()
R.id.button_next -> viewModel.onNextClick(
password = binding.editPassword.text?.toString() ?: return
)
}
}
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
return if (actionId == EditorInfo.IME_ACTION_DONE && binding.buttonNext.isEnabled) {
binding.buttonNext.performClick()
true
} else {
false
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
binding.editPassword.error = null
val isEnoughLength = (s?.length ?: 0) >= MIN_PASSWORD_LENGTH
binding.buttonNext.isEnabled = isEnoughLength
binding.layoutPassword.isHelperTextEnabled =
!isEnoughLength || viewModel.isSecondStep.value == true
}
private fun onStepChanged(isSecondStep: Boolean) {
binding.buttonCancel.isGone = isSecondStep
if (isSecondStep) {
binding.layoutPassword.helperText = getString(R.string.repeat_password)
binding.buttonNext.setText(R.string.confirm)
} else {
binding.layoutPassword.helperText = getString(R.string.password_length_hint)
binding.buttonNext.setText(R.string.next)
}
}
private companion object {
const val MIN_PASSWORD_LENGTH = 4
}
}

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.settings.protect
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.md5
class ProtectSetupViewModel(
private val settings: AppSettings
) : BaseViewModel() {
private val firstPassword = MutableStateFlow<String?>(null)
val isSecondStep = firstPassword.map {
it != null
}.asLiveDataDistinct(viewModelScope.coroutineContext)
val onPasswordSet = SingleLiveEvent<Unit>()
val onPasswordMismatch = SingleLiveEvent<Unit>()
val onClearText = SingleLiveEvent<Unit>()
fun onNextClick(password: String) {
if (firstPassword.value == null) {
firstPassword.value = password
onClearText.call(Unit)
} else {
if (firstPassword.value == password) {
settings.appPassword = password.md5()
onPasswordSet.call(Unit)
} else {
onPasswordMismatch.call(Unit)
}
}
}
}