Improve password protection
This commit is contained in:
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@@ -3,13 +3,15 @@
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="../../../../../../layout/custom_preview.xml" value="0.1" />
|
||||
<entry key="../../../../../../layout/custom_preview.xml" value="0.284375" />
|
||||
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/drawable/list_divider_material.xml" value="0.28512820512820514" />
|
||||
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/layout/simple_dropdown_item_1line.xml" value="0.24739583333333334" />
|
||||
<entry key="app/src/main/res/drawable/tab_indicator.xml" value="0.28512820512820514" />
|
||||
<entry key="app/src/main/res/drawable/tabs_background.xml" value="0.28512820512820514" />
|
||||
<entry key="app/src/main/res/layout-w600dp/fragment_details.xml" value="0.14583333333333334" />
|
||||
<entry key="app/src/main/res/layout-w600dp/fragment_list.xml" value="0.14635416666666667" />
|
||||
<entry key="app/src/main/res/layout/activity_protect.xml" value="0.26927083333333335" />
|
||||
<entry key="app/src/main/res/layout/activity_setup_protect.xml" value="0.26927083333333335" />
|
||||
<entry key="app/src/main/res/layout/dialog_favorite_categories.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/dialog_list_mode.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/fragment_chapters.xml" value="0.24739583333333334" />
|
||||
@@ -26,7 +28,9 @@
|
||||
<entry key="app/src/main/res/layout/item_page_webtoon.xml" value="0.13095238095238096" />
|
||||
<entry key="app/src/main/res/layout/item_recent.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/layout/sheet_pages.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/menu/opt_protect.xml" value="0.26927083333333335" />
|
||||
<entry key="app/src/main/res/menu/popup_category.xml" value="0.2601851851851852" />
|
||||
<entry key="app/src/main/res/xml/pref_main.xml" value="0.26927083333333335" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
@@ -78,6 +78,10 @@
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".settings.protect.ProtectSetupActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<service
|
||||
@@ -127,9 +131,11 @@
|
||||
android:resource="@xml/widget_recent" />
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
</application>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
app/src/main/res/drawable/ic_lock.xml
Normal file
11
app/src/main/res/drawable/ic_lock.xml
Normal file
@@ -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"?>
|
||||
<LinearLayout
|
||||
<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:orientation="vertical">
|
||||
android:padding="@dimen/screen_padding">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorPrimary"
|
||||
android:theme="@style/AppToolbarTheme">
|
||||
android:layout_marginTop="8dp"
|
||||
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
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:popupTheme="@style/AppPopupTheme" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
<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/enter_password"
|
||||
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="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="20dp"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
app:boxBackgroundMode="filled"
|
||||
app:endIconMode="password_toggle"
|
||||
app:errorEnabled="true"
|
||||
app:errorTextColor="@color/error">
|
||||
android:layout_marginTop="30dp"
|
||||
app:errorIconDrawable="@null"
|
||||
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:hint="@string/enter_password"
|
||||
android:gravity="center_horizontal"
|
||||
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>
|
||||
|
||||
</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>
|
||||
82
app/src/main/res/layout/activity_setup_protect.xml
Normal file
82
app/src/main/res/layout/activity_setup_protect.xml
Normal file
@@ -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>
|
||||
@@ -201,4 +201,8 @@
|
||||
<string name="auth_required">Для просмотра этого контента требуется авторизация</string>
|
||||
<string name="default_s">По умолчанию: %s</string>
|
||||
<string name="_and_x_more">…и ещё %1$d</string>
|
||||
<string name="next">Далее</string>
|
||||
<string name="protect_application_subtitle">Enter password that will be required when the application starts</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
<string name="password_length_hint">Password must be at least 4 characters</string>
|
||||
</resources>
|
||||
@@ -9,4 +9,5 @@
|
||||
<dimen name="header_height">34dp</dimen>
|
||||
<dimen name="elevation_large">16dp</dimen>
|
||||
<dimen name="list_footer_height">48dp</dimen>
|
||||
<dimen name="screen_padding">16dp</dimen>
|
||||
</resources>
|
||||
@@ -203,4 +203,8 @@
|
||||
<string name="auth_required">You should authorize to view this content</string>
|
||||
<string name="default_s">Default: %s</string>
|
||||
<string name="_and_x_more">…and %1$d more</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="protect_application_subtitle">Enter password that will be required when the application starts</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
<string name="password_length_hint">Password must be at least 4 characters</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user