Improve password protection

pull/26/head
Koitharu 5 years ago
parent 0f48ad07a3
commit 012416c881

@ -3,13 +3,15 @@
<component name="DesignSurface"> <component name="DesignSurface">
<option name="filePathToZoomLevelMap"> <option name="filePathToZoomLevelMap">
<map> <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/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="../../../../../../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/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/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_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-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_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/dialog_list_mode.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/fragment_chapters.xml" value="0.24739583333333334" /> <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_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/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/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/menu/popup_category.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/xml/pref_main.xml" value="0.26927083333333335" />
</map> </map>
</option> </option>
</component> </component>

@ -78,6 +78,10 @@
android:label="@string/search" /> android:label="@string/search" />
<activity <activity
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity" android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
android:noHistory="true"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".settings.protect.ProtectSetupActivity"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<service <service
@ -127,9 +131,11 @@
android:resource="@xml/widget_recent" /> android:resource="@xml/widget_recent" />
</receiver> </receiver>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" <meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" /> android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut" <meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" /> android:value="true" />
</application> </application>

@ -23,6 +23,7 @@ import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule import org.koitharu.kotatsu.main.mainModule
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.reader.readerModule import org.koitharu.kotatsu.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule import org.koitharu.kotatsu.remotelist.remoteListModule
import org.koitharu.kotatsu.search.searchModule import org.koitharu.kotatsu.search.searchModule
@ -55,6 +56,7 @@ class KotatsuApp : Application() {
initKoin() initKoin()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext)) Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme) AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
val widgetUpdater = WidgetUpdater(applicationContext) val widgetUpdater = WidgetUpdater(applicationContext)
FavouritesRepository.subscribe(widgetUpdater) FavouritesRepository.subscribe(widgetUpdater)
HistoryRepository.subscribe(widgetUpdater) HistoryRepository.subscribe(widgetUpdater)

@ -27,7 +27,6 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
protected lateinit var binding: B protected lateinit var binding: B
private set private set
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) { protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(this, supportFragmentManager) ExceptionResolver(this, supportFragmentManager)
} }
@ -60,7 +59,9 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
} }
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { 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 return insets
} }

@ -13,5 +13,5 @@ val mainModule
single { AppProtectHelper(get()) } single { AppProtectHelper(get()) }
single { ShortcutsRepository(androidContext(), get(), get(), get()) } single { ShortcutsRepository(androidContext(), get(), get(), get()) }
viewModel { MainViewModel(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 androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity 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.favourites.ui.FavouritesContainerFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.local.ui.LocalListFragment 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.reader.ui.ReaderActivity
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.ui.SearchHelper import org.koitharu.kotatsu.search.ui.SearchHelper
@ -45,17 +43,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
View.OnClickListener { View.OnClickListener {
private val viewModel by viewModel<MainViewModel>() private val viewModel by viewModel<MainViewModel>()
private val protectHelper by inject<AppProtectHelper>()
private lateinit var drawerToggle: ActionBarDrawerToggle private lateinit var drawerToggle: ActionBarDrawerToggle
private var closeable: Closeable? = null private var closeable: Closeable? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (protectHelper.check(this)) {
finish()
return
}
setContentView(ActivityMainBinding.inflate(layoutInflater)) setContentView(ActivityMainBinding.inflate(layoutInflater))
drawerToggle = drawerToggle =
ActionBarDrawerToggle( ActionBarDrawerToggle(
@ -93,7 +86,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
override fun onDestroy() { override fun onDestroy() {
closeable?.close() closeable?.close()
protectHelper.lock()
super.onDestroy() super.onDestroy()
} }

@ -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()
} }
} }

@ -6,13 +6,10 @@ import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.View
import android.view.MenuItem
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView import android.widget.TextView
import androidx.core.graphics.Insets 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.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
@ -20,50 +17,49 @@ import org.koitharu.kotatsu.databinding.ActivityProtectBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEditorActionListener, class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEditorActionListener,
TextWatcher { TextWatcher, View.OnClickListener {
private val viewModel by viewModel<ProtectViewModel>() private val viewModel by viewModel<ProtectViewModel>()
private val appProtectHelper by inject<AppProtectHelper>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityProtectBinding.inflate(layoutInflater)) setContentView(ActivityProtectBinding.inflate(layoutInflater))
binding.editPassword.setOnEditorActionListener(this) binding.editPassword.setOnEditorActionListener(this)
binding.editPassword.addTextChangedListener(this) binding.editPassword.addTextChangedListener(this)
supportActionBar?.run { binding.buttonNext.setOnClickListener(this)
setDisplayHomeAsUpEnabled(true) binding.buttonCancel.setOnClickListener(this)
setHomeAsUpIndicator(R.drawable.ic_cross)
}
viewModel.onError.observe(this, this::onError) viewModel.onError.observe(this, this::onError)
viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.onUnlockSuccess.observe(this, this::onUnlockSuccess) viewModel.onUnlockSuccess.observe(this) {
} val intent = intent.getParcelableExtra<Intent>(EXTRA_INTENT)
startActivity(intent)
override fun onCreateOptionsMenu(menu: Menu?): Boolean { finishAfterTransition()
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
} }
else -> super.onOptionsItemSelected(item)
binding.editPassword.requestFocus()
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.toolbar.updatePadding( val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
left = insets.left, binding.root.setPadding(
right = insets.right, basePadding + insets.left,
top = insets.top 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 { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
return if (actionId == EditorInfo.IME_ACTION_DONE) { return if (actionId == EditorInfo.IME_ACTION_DONE && binding.buttonNext.isEnabled) {
viewModel.tryUnlock(binding.editPassword.text.toString().orEmpty()) binding.buttonNext.performClick()
true true
} else { } else {
false false
@ -76,10 +72,7 @@ class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEdito
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
binding.layoutPassword.error = null binding.layoutPassword.error = null
} binding.buttonNext.isEnabled = !s.isNullOrEmpty()
private fun onUnlockSuccess(unit: Unit) {
appProtectHelper.unlock(this)
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
@ -92,6 +85,11 @@ class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEdito
companion object { 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 package org.koitharu.kotatsu.main.ui.protect
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
@ -8,16 +9,23 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.md5 import org.koitharu.kotatsu.utils.ext.md5
class ProtectViewModel( class ProtectViewModel(
private val settings: AppSettings private val settings: AppSettings,
private val protectHelper: AppProtectHelper,
) : BaseViewModel() { ) : BaseViewModel() {
private var job: Job? = null
val onUnlockSuccess = SingleLiveEvent<Unit>() val onUnlockSuccess = SingleLiveEvent<Unit>()
fun tryUnlock(password: String) { fun tryUnlock(password: String) {
launchLoadingJob { if (job?.isActive == true) {
return
}
job = launchLoadingJob {
val passwordHash = password.md5() val passwordHash = password.md5()
val appPasswordHash = settings.appPassword val appPasswordHash = settings.appPassword
if (passwordHash == appPasswordHash) { if (passwordHash == appPasswordHash) {
protectHelper.unlock()
onUnlockSuccess.call(Unit) onUnlockSuccess.call(Unit)
} else { } else {
delay(PASSWORD_COMPARE_DELAY) delay(PASSWORD_COMPARE_DELAY)

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.text.InputType 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.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.io.File import java.io.File
@ -77,6 +79,10 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
?: getString(R.string.not_available) ?: 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 true
} }
AppSettings.KEY_PROTECT_APP -> { AppSettings.KEY_PROTECT_APP -> {
if ((preference as? SwitchPreference ?: return false).isChecked) { val pref = (preference as? SwitchPreference ?: return false)
enableAppProtection(preference) if (pref.isChecked) {
pref.isChecked = false
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
} else { } else {
settings.appPassword = null 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.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.backup.BackupViewModel import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
val settingsModule val settingsModule
get() = module { get() = module {
@ -19,4 +20,5 @@ val settingsModule
viewModel { BackupViewModel(get(), androidContext()) } viewModel { BackupViewModel(get(), androidContext()) }
viewModel { (uri: Uri?) -> RestoreViewModel(uri, 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)
}
}
}
}

@ -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>

@ -201,4 +201,8 @@
<string name="auth_required">Для просмотра этого контента требуется авторизация</string> <string name="auth_required">Для просмотра этого контента требуется авторизация</string>
<string name="default_s">По умолчанию: %s</string> <string name="default_s">По умолчанию: %s</string>
<string name="_and_x_more">…и ещё %1$d</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> </resources>

@ -9,4 +9,5 @@
<dimen name="header_height">34dp</dimen> <dimen name="header_height">34dp</dimen>
<dimen name="elevation_large">16dp</dimen> <dimen name="elevation_large">16dp</dimen>
<dimen name="list_footer_height">48dp</dimen> <dimen name="list_footer_height">48dp</dimen>
<dimen name="screen_padding">16dp</dimen>
</resources> </resources>

@ -203,4 +203,8 @@
<string name="auth_required">You should authorize to view this content</string> <string name="auth_required">You should authorize to view this content</string>
<string name="default_s">Default: %s</string> <string name="default_s">Default: %s</string>
<string name="_and_x_more">…and %1$d more</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> </resources>
Loading…
Cancel
Save