Improve password protection
This commit is contained in:
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.localModule
|
||||
import org.koitharu.kotatsu.main.mainModule
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.reader.readerModule
|
||||
import org.koitharu.kotatsu.remotelist.remoteListModule
|
||||
import org.koitharu.kotatsu.search.searchModule
|
||||
@@ -55,6 +56,7 @@ class KotatsuApp : Application() {
|
||||
initKoin()
|
||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
||||
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
||||
FavouritesRepository.subscribe(widgetUpdater)
|
||||
HistoryRepository.subscribe(widgetUpdater)
|
||||
|
||||
@@ -27,7 +27,6 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
|
||||
protected lateinit var binding: B
|
||||
private set
|
||||
|
||||
|
||||
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ExceptionResolver(this, supportFragmentManager)
|
||||
}
|
||||
@@ -60,7 +59,9 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
onWindowInsetsChanged(insets.getInsets(WindowInsetsCompat.Type.systemBars()))
|
||||
val baseInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||
onWindowInsetsChanged(Insets.max(baseInsets, imeInsets))
|
||||
return insets
|
||||
}
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@ val mainModule
|
||||
single { AppProtectHelper(get()) }
|
||||
single { ShortcutsRepository(androidContext(), get(), get(), get()) }
|
||||
viewModel { MainViewModel(get(), get()) }
|
||||
viewModel { ProtectViewModel(get()) }
|
||||
viewModel { ProtectViewModel(get(), get()) }
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
@@ -28,7 +27,6 @@ import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.FavouritesContainerFragment
|
||||
import org.koitharu.kotatsu.history.ui.HistoryListFragment
|
||||
import org.koitharu.kotatsu.local.ui.LocalListFragment
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
import org.koitharu.kotatsu.search.ui.SearchHelper
|
||||
@@ -45,17 +43,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
View.OnClickListener {
|
||||
|
||||
private val viewModel by viewModel<MainViewModel>()
|
||||
private val protectHelper by inject<AppProtectHelper>()
|
||||
|
||||
private lateinit var drawerToggle: ActionBarDrawerToggle
|
||||
private var closeable: Closeable? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (protectHelper.check(this)) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
setContentView(ActivityMainBinding.inflate(layoutInflater))
|
||||
drawerToggle =
|
||||
ActionBarDrawerToggle(
|
||||
@@ -93,7 +86,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
|
||||
override fun onDestroy() {
|
||||
closeable?.close()
|
||||
protectHelper.lock()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,58 @@
|
||||
package org.koitharu.kotatsu.main.ui.protect
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
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 activityCounter = 0
|
||||
|
||||
fun unlock(activity: Activity) {
|
||||
isUnlocked = true
|
||||
with(activity) {
|
||||
startActivity(Intent(this, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (activity is ProtectActivity) {
|
||||
return
|
||||
}
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
if (activity is ProtectActivity) {
|
||||
return
|
||||
}
|
||||
activityCounter--
|
||||
if (activityCounter == 0) {
|
||||
restoreLock()
|
||||
}
|
||||
}
|
||||
|
||||
fun unlock() {
|
||||
isUnlocked = true
|
||||
}
|
||||
|
||||
private fun restoreLock() {
|
||||
isUnlocked = settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun check(activity: Activity): Boolean {
|
||||
return if (!isUnlocked) {
|
||||
activity.startActivity(ProtectActivity.newIntent(activity)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,10 @@ import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
@@ -20,50 +17,49 @@ import org.koitharu.kotatsu.databinding.ActivityProtectBinding
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEditorActionListener,
|
||||
TextWatcher {
|
||||
TextWatcher, View.OnClickListener {
|
||||
|
||||
private val viewModel by viewModel<ProtectViewModel>()
|
||||
private val appProtectHelper by inject<AppProtectHelper>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityProtectBinding.inflate(layoutInflater))
|
||||
binding.editPassword.setOnEditorActionListener(this)
|
||||
binding.editPassword.addTextChangedListener(this)
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(R.drawable.ic_cross)
|
||||
}
|
||||
binding.buttonNext.setOnClickListener(this)
|
||||
binding.buttonCancel.setOnClickListener(this)
|
||||
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.onUnlockSuccess.observe(this, this::onUnlockSuccess)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.opt_protect, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_done -> {
|
||||
viewModel.tryUnlock(binding.editPassword.text.toString().orEmpty())
|
||||
true
|
||||
viewModel.onUnlockSuccess.observe(this) {
|
||||
val intent = intent.getParcelableExtra<Intent>(EXTRA_INTENT)
|
||||
startActivity(intent)
|
||||
finishAfterTransition()
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
|
||||
binding.editPassword.requestFocus()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.toolbar.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
top = insets.top
|
||||
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_next -> viewModel.tryUnlock(binding.editPassword.text?.toString().orEmpty())
|
||||
R.id.button_cancel -> finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
return if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
viewModel.tryUnlock(binding.editPassword.text.toString().orEmpty())
|
||||
return if (actionId == EditorInfo.IME_ACTION_DONE && binding.buttonNext.isEnabled) {
|
||||
binding.buttonNext.performClick()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -76,10 +72,7 @@ class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEdito
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
binding.layoutPassword.error = null
|
||||
}
|
||||
|
||||
private fun onUnlockSuccess(unit: Unit) {
|
||||
appProtectHelper.unlock(this)
|
||||
binding.buttonNext.isEnabled = !s.isNullOrEmpty()
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
@@ -92,6 +85,11 @@ class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEdito
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, ProtectActivity::class.java)
|
||||
private const val EXTRA_INTENT = "src_intent"
|
||||
|
||||
fun newIntent(context: Context, sourceIntent: Intent): Intent {
|
||||
return Intent(context, ProtectActivity::class.java)
|
||||
.putExtra(EXTRA_INTENT, sourceIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.main.ui.protect
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
|
||||
@@ -8,16 +9,23 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.md5
|
||||
|
||||
class ProtectViewModel(
|
||||
private val settings: AppSettings
|
||||
private val settings: AppSettings,
|
||||
private val protectHelper: AppProtectHelper,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
val onUnlockSuccess = SingleLiveEvent<Unit>()
|
||||
|
||||
fun tryUnlock(password: String) {
|
||||
launchLoadingJob {
|
||||
if (job?.isActive == true) {
|
||||
return
|
||||
}
|
||||
job = launchLoadingJob {
|
||||
val passwordHash = password.md5()
|
||||
val appPasswordHash = settings.appPassword
|
||||
if (passwordHash == appPasswordHash) {
|
||||
protectHelper.unlock()
|
||||
onUnlockSuccess.call(Unit)
|
||||
} else {
|
||||
delay(PASSWORD_COMPARE_DELAY)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user