Improve password protection
parent
0f48ad07a3
commit
012416c881
@ -1,33 +1,58 @@
|
|||||||
package org.koitharu.kotatsu.main.ui.protect
|
package org.koitharu.kotatsu.main.ui.protect
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
|
||||||
|
|
||||||
class AppProtectHelper(private val settings: AppSettings) {
|
class AppProtectHelper(private val settings: AppSettings) : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
private var isUnlocked = settings.appPassword.isNullOrEmpty()
|
private var isUnlocked = settings.appPassword.isNullOrEmpty()
|
||||||
|
private var activityCounter = 0
|
||||||
|
|
||||||
fun unlock(activity: Activity) {
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
isUnlocked = true
|
if (activity is ProtectActivity) {
|
||||||
with(activity) {
|
return
|
||||||
startActivity(Intent(this, MainActivity::class.java)
|
}
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
|
activityCounter++
|
||||||
|
if (!isUnlocked) {
|
||||||
|
val sourceIntent = Intent(activity, activity.javaClass)
|
||||||
|
activity.intent?.let {
|
||||||
|
sourceIntent.putExtras(it)
|
||||||
|
sourceIntent.action = it.action
|
||||||
|
sourceIntent.setDataAndType(it.data, it.type)
|
||||||
|
}
|
||||||
|
activity.startActivity(ProtectActivity.newIntent(activity, sourceIntent))
|
||||||
|
activity.finishAfterTransition()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lock() {
|
override fun onActivityStarted(activity: Activity) = Unit
|
||||||
isUnlocked = settings.appPassword.isNullOrEmpty()
|
|
||||||
}
|
override fun onActivityResumed(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityPaused(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||||
|
|
||||||
fun check(activity: Activity): Boolean {
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
return if (!isUnlocked) {
|
if (activity is ProtectActivity) {
|
||||||
activity.startActivity(ProtectActivity.newIntent(activity)
|
return
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
activityCounter--
|
||||||
|
if (activityCounter == 0) {
|
||||||
|
restoreLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unlock() {
|
||||||
|
isUnlocked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreLock() {
|
||||||
|
isUnlocked = settings.appPassword.isNullOrEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM9,6c0,-1.66 1.34,-3 3,-3s3,1.34 3,3v2L9,8L9,6zM18,20L6,20L6,10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z" />
|
||||||
|
</vector>
|
||||||
@ -1,47 +1,80 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:padding="@dimen/screen_padding">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<TextView
|
||||||
android:id="@+id/appbar"
|
android:id="@+id/textView_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?colorPrimary"
|
android:layout_marginTop="8dp"
|
||||||
android:theme="@style/AppToolbarTheme">
|
android:drawablePadding="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/app_name"
|
||||||
|
android:textAppearance="?textAppearanceHeadline5"
|
||||||
|
app:drawableTint="?colorPrimary"
|
||||||
|
app:drawableTopCompat="@drawable/ic_lock"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<TextView
|
||||||
android:id="@id/toolbar"
|
android:id="@+id/textView_subtitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_scrollFlags="scroll|enterAlways"
|
android:layout_marginTop="12dp"
|
||||||
app:popupTheme="@style/AppPopupTheme" />
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/enter_password"
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
android:textAppearance="?textAppearanceSubtitle1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView_title" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/layout_password"
|
android:id="@+id/layout_password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_marginTop="30dp"
|
||||||
android:layout_margin="20dp"
|
app:errorIconDrawable="@null"
|
||||||
app:boxBackgroundColor="@android:color/transparent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:boxBackgroundMode="filled"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:endIconMode="password_toggle"
|
app:layout_constraintTop_toBottomOf="@id/textView_subtitle">
|
||||||
app:errorEnabled="true"
|
|
||||||
app:errorTextColor="@color/error">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/edit_password"
|
android:id="@+id/edit_password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/enter_password"
|
android:gravity="center_horizontal"
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
android:inputType="textPassword" />
|
android:inputType="textPassword"
|
||||||
|
android:maxLength="24"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="1234" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
<Button
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@android:string/cancel"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="@string/next"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="@dimen/screen_padding">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:drawablePadding="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/protect_application"
|
||||||
|
android:textAppearance="?textAppearanceHeadline5"
|
||||||
|
app:drawableTint="?colorPrimary"
|
||||||
|
app:drawableTopCompat="@drawable/ic_lock"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_subtitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:text="@string/protect_application_subtitle"
|
||||||
|
android:textAppearance="?textAppearanceSubtitle1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView_title" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_password"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
app:errorIconDrawable="@null"
|
||||||
|
app:helperText="@string/password_length_hint"
|
||||||
|
app:hintEnabled="false"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textView_subtitle">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/edit_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLength="24"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="1234" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@android:string/cancel"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="@string/next"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_done"
|
|
||||||
android:icon="@drawable/ic_done"
|
|
||||||
android:orderInCategory="0"
|
|
||||||
android:title="@string/done"
|
|
||||||
app:showAsAction="ifRoom|withText" />
|
|
||||||
</menu>
|
|
||||||
Loading…
Reference in New Issue