Improve password protection

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

6
.idea/misc.xml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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