Merge pull request #238 from PhilKes/fix/user-unenrolls-biometric

Handle user un-enrolling device biometrics while biometric lock is enabled
This commit is contained in:
Phil 2025-01-16 22:09:19 +01:00 committed by GitHub
commit b55c01c94a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 222 additions and 145 deletions

View file

@ -3,19 +3,27 @@ package com.philkes.notallyx.presentation.activity
import android.app.Activity import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Intent import android.content.Intent
import android.hardware.biometrics.BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT
import android.hardware.biometrics.BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philkes.notallyx.NotallyXApplication import com.philkes.notallyx.NotallyXApplication
import com.philkes.notallyx.R import com.philkes.notallyx.R
import com.philkes.notallyx.presentation.showToast
import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel
import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
import com.philkes.notallyx.utils.security.disableBiometricLock
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() { abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
@ -26,6 +34,7 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: T protected lateinit var binding: T
protected lateinit var preferences: NotallyXPreferences protected lateinit var preferences: NotallyXPreferences
protected val baseModel: BaseNoteModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -68,8 +77,41 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
biometricAuthenticationActivityResultLauncher, biometricAuthenticationActivityResultLauncher,
R.string.unlock, R.string.unlock,
onSuccess = { unlock() }, onSuccess = { unlock() },
) { ) { errorCode ->
finish() when (errorCode) {
BIOMETRIC_ERROR_NO_BIOMETRICS -> {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.unlock_with_biometrics_not_setup)
.setPositiveButton(R.string.disable) { _, _ ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
disableBiometricLock(baseModel)
}
show()
}
.setNegativeButton(R.string.tap_to_set_up) { _, _ ->
val intent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Intent(Settings.ACTION_BIOMETRIC_ENROLL)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Intent(Settings.ACTION_FINGERPRINT_ENROLL)
} else {
Intent(Settings.ACTION_SECURITY_SETTINGS)
}
startActivity(intent)
}
.show()
}
BIOMETRIC_ERROR_HW_NOT_PRESENT -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
disableBiometricLock(baseModel)
showToast(R.string.biometrics_disable_success)
}
show()
}
else -> finish()
}
} }
} }

View file

@ -17,7 +17,6 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
@ -77,11 +76,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
private lateinit var exportFileActivityResultLauncher: ActivityResultLauncher<Intent> private lateinit var exportFileActivityResultLauncher: ActivityResultLauncher<Intent>
private lateinit var exportNotesActivityResultLauncher: ActivityResultLauncher<Intent> private lateinit var exportNotesActivityResultLauncher: ActivityResultLauncher<Intent>
private val model: BaseNoteModel by viewModels()
private val actionModeCancelCallback = private val actionModeCancelCallback =
object : OnBackPressedCallback(true) { object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
model.actionMode.close(true) baseModel.actionMode.close(true)
} }
} }
@ -159,10 +157,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
.setCheckable(true) .setCheckable(true)
.setIcon(R.drawable.settings) .setIcon(R.drawable.settings)
} }
model.preferences.labelsHiddenInNavigation.observe(this) { hiddenLabels -> baseModel.preferences.labelsHiddenInNavigation.observe(this) { hiddenLabels ->
hideLabelsInNavigation(hiddenLabels, model.preferences.maxLabels.value) hideLabelsInNavigation(hiddenLabels, baseModel.preferences.maxLabels.value)
} }
model.preferences.maxLabels.observe(this) { maxLabels -> baseModel.preferences.maxLabels.observe(this) { maxLabels ->
binding.NavigationView.menu.setupLabelsMenuItems(labels, maxLabels) binding.NavigationView.menu.setupLabelsMenuItems(labels, maxLabels)
} }
} }
@ -201,7 +199,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
} else null } else null
configuration = AppBarConfiguration(binding.NavigationView.menu, binding.DrawerLayout) configuration = AppBarConfiguration(binding.NavigationView.menu, binding.DrawerLayout)
setupActionBarWithNavController(navController, configuration) setupActionBarWithNavController(navController, configuration)
hideLabelsInNavigation(model.preferences.labelsHiddenInNavigation.value, maxLabelsToDisplay) hideLabelsInNavigation(
baseModel.preferences.labelsHiddenInNavigation.value,
maxLabelsToDisplay,
)
} }
private fun hideLabelsInNavigation(hiddenLabels: Set<String>, maxLabelsToDisplay: Int) { private fun hideLabelsInNavigation(hiddenLabels: Set<String>, maxLabelsToDisplay: Int) {
@ -218,7 +219,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
} }
private fun setupActionMode() { private fun setupActionMode() {
binding.ActionMode.setNavigationOnClickListener { model.actionMode.close(true) } binding.ActionMode.setNavigationOnClickListener { baseModel.actionMode.close(true) }
val transition = val transition =
MaterialFade().apply { MaterialFade().apply {
@ -230,7 +231,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
excludeTarget(binding.NavigationView, true) excludeTarget(binding.NavigationView, true)
} }
model.actionMode.enabled.observe(this) { enabled -> baseModel.actionMode.enabled.observe(this) { enabled ->
TransitionManager.beginDelayedTransition(binding.RelativeLayout, transition) TransitionManager.beginDelayedTransition(binding.RelativeLayout, transition)
if (enabled) { if (enabled) {
binding.Toolbar.visibility = View.GONE binding.Toolbar.visibility = View.GONE
@ -245,23 +246,23 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
} }
val menu = binding.ActionMode.menu val menu = binding.ActionMode.menu
model.folder.observe(this@MainActivity, ModelFolderObserver(menu, model)) baseModel.folder.observe(this@MainActivity, ModelFolderObserver(menu, baseModel))
} }
private fun moveNotes(folderTo: Folder) { private fun moveNotes(folderTo: Folder) {
val folderFrom = model.actionMode.getFirstNote().folder val folderFrom = baseModel.actionMode.getFirstNote().folder
val ids = model.moveBaseNotes(folderTo) val ids = baseModel.moveBaseNotes(folderTo)
Snackbar.make( Snackbar.make(
findViewById(R.id.DrawerLayout), findViewById(R.id.DrawerLayout),
getQuantityString(folderTo.movedToResId(), ids.size), getQuantityString(folderTo.movedToResId(), ids.size),
Snackbar.LENGTH_SHORT, Snackbar.LENGTH_SHORT,
) )
.apply { setAction(R.string.undo) { model.moveBaseNotes(ids, folderFrom) } } .apply { setAction(R.string.undo) { baseModel.moveBaseNotes(ids, folderFrom) } }
.show() .show()
} }
private fun share() { private fun share() {
val baseNote = model.actionMode.getFirstNote() val baseNote = baseModel.actionMode.getFirstNote()
val body = val body =
when (baseNote.type) { when (baseNote.type) {
Type.NOTE -> baseNote.body.applySpans(baseNote.spans) Type.NOTE -> baseNote.body.applySpans(baseNote.spans)
@ -273,19 +274,19 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
private fun deleteForever() { private fun deleteForever() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setMessage(R.string.delete_selected_notes) .setMessage(R.string.delete_selected_notes)
.setPositiveButton(R.string.delete) { _, _ -> model.deleteSelectedBaseNotes() } .setPositiveButton(R.string.delete) { _, _ -> baseModel.deleteSelectedBaseNotes() }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
} }
private fun label() { private fun label() {
val baseNotes = model.actionMode.selectedNotes.values val baseNotes = baseModel.actionMode.selectedNotes.values
lifecycleScope.launch { lifecycleScope.launch {
val labels = model.getAllLabels() val labels = baseModel.getAllLabels()
if (labels.isNotEmpty()) { if (labels.isNotEmpty()) {
displaySelectLabelsDialog(labels, baseNotes) displaySelectLabelsDialog(labels, baseNotes)
} else { } else {
model.actionMode.close(true) baseModel.actionMode.close(true)
navigateWithAnimation(R.id.Labels) navigateWithAnimation(R.id.Labels)
} }
} }
@ -340,15 +341,15 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
noteLabels noteLabels
} }
baseNotes.zip(updatedBaseNotesLabels).forEach { (baseNote, updatedLabels) -> baseNotes.zip(updatedBaseNotesLabels).forEach { (baseNote, updatedLabels) ->
model.updateBaseNoteLabels(updatedLabels, baseNote.id) baseModel.updateBaseNoteLabels(updatedLabels, baseNote.id)
} }
} }
.show() .show()
} }
private fun exportSelectedNotes(mimeType: ExportMimeType) { private fun exportSelectedNotes(mimeType: ExportMimeType) {
if (model.actionMode.count.value == 1) { if (baseModel.actionMode.count.value == 1) {
val baseNote = model.actionMode.getFirstNote() val baseNote = baseModel.actionMode.getFirstNote()
when (mimeType) { when (mimeType) {
ExportMimeType.PDF -> { ExportMimeType.PDF -> {
exportPdfFile( exportPdfFile(
@ -392,7 +393,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.apply { addCategory(Intent.CATEGORY_DEFAULT) } .apply { addCategory(Intent.CATEGORY_DEFAULT) }
.wrapWithChooser(this@MainActivity) .wrapWithChooser(this@MainActivity)
model.selectedExportMimeType = mimeType baseModel.selectedExportMimeType = mimeType
exportNotesActivityResultLauncher.launch(intent) exportNotesActivityResultLauncher.launch(intent)
} }
} }
@ -425,7 +426,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
putExtra(Intent.EXTRA_TITLE, file.nameWithoutExtension!!) putExtra(Intent.EXTRA_TITLE, file.nameWithoutExtension!!)
} }
.wrapWithChooser(this@MainActivity) .wrapWithChooser(this@MainActivity)
model.selectedExportFile = file baseModel.selectedExportFile = file
exportFileActivityResultLauncher.launch(intent) exportFileActivityResultLauncher.launch(intent)
} }
@ -521,22 +522,26 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
private fun setupSearch() { private fun setupSearch() {
binding.EnterSearchKeyword.apply { binding.EnterSearchKeyword.apply {
setText(model.keyword) setText(baseModel.keyword)
doAfterTextChanged { text -> doAfterTextChanged { text ->
model.keyword = requireNotNull(text).trim().toString() baseModel.keyword = requireNotNull(text).trim().toString()
if ( if (
model.keyword.isNotEmpty() && baseModel.keyword.isNotEmpty() &&
navController.currentDestination?.id != R.id.Search navController.currentDestination?.id != R.id.Search
) { ) {
val bundle = val bundle =
Bundle().apply { putSerializable(EXTRA_INITIAL_FOLDER, model.folder.value) } Bundle().apply {
putSerializable(EXTRA_INITIAL_FOLDER, baseModel.folder.value)
}
navController.navigate(R.id.Search, bundle) navController.navigate(R.id.Search, bundle)
} }
} }
setOnFocusChangeListener { v, hasFocus -> setOnFocusChangeListener { v, hasFocus ->
if (hasFocus && navController.currentDestination?.id != R.id.Search) { if (hasFocus && navController.currentDestination?.id != R.id.Search) {
val bundle = val bundle =
Bundle().apply { putSerializable(EXTRA_INITIAL_FOLDER, model.folder.value) } Bundle().apply {
putSerializable(EXTRA_INITIAL_FOLDER, baseModel.folder.value)
}
navController.navigate(R.id.Search, bundle) navController.navigate(R.id.Search, bundle)
} }
} }
@ -547,13 +552,13 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
exportFileActivityResultLauncher = exportFileActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> model.exportSelectedFileToUri(uri) } result.data?.data?.let { uri -> baseModel.exportSelectedFileToUri(uri) }
} }
} }
exportNotesActivityResultLauncher = exportNotesActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> model.exportSelectedNotesToFolder(uri) } result.data?.data?.let { uri -> baseModel.exportSelectedNotesToFolder(uri) }
} }
} }
} }

View file

@ -42,7 +42,7 @@ import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreference
import com.philkes.notallyx.utils.Operations import com.philkes.notallyx.utils.Operations
import com.philkes.notallyx.utils.Operations.catchNoBrowserInstalled import com.philkes.notallyx.utils.Operations.catchNoBrowserInstalled
import com.philkes.notallyx.utils.Operations.reportBug import com.philkes.notallyx.utils.Operations.reportBug
import com.philkes.notallyx.utils.security.decryptDatabase import com.philkes.notallyx.utils.security.disableBiometricLock
import com.philkes.notallyx.utils.security.encryptDatabase import com.philkes.notallyx.utils.security.encryptDatabase
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
import com.philkes.notallyx.utils.wrapWithChooser import com.philkes.notallyx.utils.wrapWithChooser
@ -618,6 +618,10 @@ class SettingsFragment : Fragment() {
model.savePreference(model.preferences.iv, cipher.iv) model.savePreference(model.preferences.iv, cipher.iv)
val passphrase = model.preferences.databaseEncryptionKey.init(cipher) val passphrase = model.preferences.databaseEncryptionKey.init(cipher)
encryptDatabase(requireContext(), passphrase) encryptDatabase(requireContext(), passphrase)
model.savePreference(
model.preferences.fallbackDatabaseEncryptionKey,
passphrase,
)
model.savePreference(model.preferences.biometricLock, BiometricLock.ENABLED) model.savePreference(model.preferences.biometricLock, BiometricLock.ENABLED)
} }
val app = (activity?.application as NotallyXApplication) val app = (activity?.application as NotallyXApplication)
@ -638,11 +642,7 @@ class SettingsFragment : Fragment() {
model.preferences.iv.value!!, model.preferences.iv.value!!,
onSuccess = { cipher -> onSuccess = { cipher ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val encryptedPassphrase = model.preferences.databaseEncryptionKey.value requireContext().disableBiometricLock(model, cipher)
val passphrase = cipher.doFinal(encryptedPassphrase)
model.closeDatabase()
decryptDatabase(requireContext(), passphrase)
model.savePreference(model.preferences.biometricLock, BiometricLock.DISABLED)
} }
showToast(R.string.biometrics_disable_success) showToast(R.string.biometrics_disable_success)
}, },

View file

@ -85,7 +85,7 @@ abstract class EditActivity(private val type: Type) :
private val searchResultPos = NotNullLiveData(-1) private val searchResultPos = NotNullLiveData(-1)
private val searchResultsAmount = NotNullLiveData(-1) private val searchResultsAmount = NotNullLiveData(-1)
internal val model: NotallyModel by viewModels() internal val notallyModel: NotallyModel by viewModels()
internal lateinit var changeHistory: ChangeHistory internal lateinit var changeHistory: ChangeHistory
protected val undos: MutableList<View> = mutableListOf() protected val undos: MutableList<View> = mutableListOf()
@ -93,9 +93,9 @@ abstract class EditActivity(private val type: Type) :
override fun finish() { override fun finish() {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if (model.isEmpty()) { if (notallyModel.isEmpty()) {
model.deleteBaseNote() notallyModel.deleteBaseNote()
} else if (model.isModified()) { } else if (notallyModel.isModified()) {
saveNote() saveNote()
} }
super.finish() super.finish()
@ -104,21 +104,21 @@ abstract class EditActivity(private val type: Type) :
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong("id", model.id) outState.putLong("id", notallyModel.id)
if (model.isModified()) { if (notallyModel.isModified()) {
lifecycleScope.launch { saveNote() } lifecycleScope.launch { saveNote() }
} }
} }
open suspend fun saveNote() { open suspend fun saveNote() {
model.modifiedTimestamp = System.currentTimeMillis() notallyModel.modifiedTimestamp = System.currentTimeMillis()
model.saveNote() notallyModel.saveNote()
WidgetProvider.sendBroadcast(application, longArrayOf(model.id)) WidgetProvider.sendBroadcast(application, longArrayOf(notallyModel.id))
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
model.type = type notallyModel.type = type
initialiseBinding() initialiseBinding()
setContentView(binding.root) setContentView(binding.root)
@ -126,12 +126,14 @@ abstract class EditActivity(private val type: Type) :
val persistedId = savedInstanceState?.getLong("id") val persistedId = savedInstanceState?.getLong("id")
val selectedId = intent.getLongExtra(Constants.SelectedBaseNote, 0L) val selectedId = intent.getLongExtra(Constants.SelectedBaseNote, 0L)
val id = persistedId ?: selectedId val id = persistedId ?: selectedId
model.setState(id) notallyModel.setState(id)
if (model.isNewNote && intent.action == Intent.ACTION_SEND) { if (notallyModel.isNewNote && intent.action == Intent.ACTION_SEND) {
handleSharedNote() handleSharedNote()
} else if (model.isNewNote) { } else if (notallyModel.isNewNote) {
intent.getStringExtra(Constants.SelectedLabel)?.let { model.setLabels(listOf(it)) } intent.getStringExtra(Constants.SelectedLabel)?.let {
notallyModel.setLabels(listOf(it))
}
} }
setupToolbars() setupToolbars()
@ -152,7 +154,7 @@ abstract class EditActivity(private val type: Type) :
recordAudioActivityResultLauncher = recordAudioActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
model.addAudio() notallyModel.addAudio()
} }
} }
addImagesActivityResultLauncher = addImagesActivityResultLauncher =
@ -162,11 +164,11 @@ abstract class EditActivity(private val type: Type) :
val clipData = result.data?.clipData val clipData = result.data?.clipData
if (uri != null) { if (uri != null) {
val uris = arrayOf(uri) val uris = arrayOf(uri)
model.addImages(uris) notallyModel.addImages(uris)
} else if (clipData != null) { } else if (clipData != null) {
val uris = val uris =
Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri } Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
model.addImages(uris) notallyModel.addImages(uris)
} }
} }
} }
@ -182,7 +184,7 @@ abstract class EditActivity(private val type: Type) :
) )
} }
if (!list.isNullOrEmpty()) { if (!list.isNullOrEmpty()) {
model.deleteImages(list) notallyModel.deleteImages(list)
} }
} }
} }
@ -191,12 +193,12 @@ abstract class EditActivity(private val type: Type) :
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
val list = val list =
result.data?.getStringArrayListExtra(SelectLabelsActivity.SELECTED_LABELS) result.data?.getStringArrayListExtra(SelectLabelsActivity.SELECTED_LABELS)
if (list != null && list != model.labels) { if (list != null && list != notallyModel.labels) {
model.setLabels(list) notallyModel.setLabels(list)
Operations.bindLabels( Operations.bindLabels(
binding.LabelGroup, binding.LabelGroup,
model.labels, notallyModel.labels,
model.textSize, notallyModel.textSize,
paddingTop = true, paddingTop = true,
) )
} }
@ -214,7 +216,7 @@ abstract class EditActivity(private val type: Type) :
) )
} }
if (audio != null) { if (audio != null) {
model.deleteAudio(audio) notallyModel.deleteAudio(audio)
} }
} }
} }
@ -225,11 +227,11 @@ abstract class EditActivity(private val type: Type) :
val clipData = result.data?.clipData val clipData = result.data?.clipData
if (uri != null) { if (uri != null) {
val uris = arrayOf(uri) val uris = arrayOf(uri)
model.addFiles(uris) notallyModel.addFiles(uris)
} else if (clipData != null) { } else if (clipData != null) {
val uris = val uris =
Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri } Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
model.addFiles(uris) notallyModel.addFiles(uris)
} }
} }
} }
@ -276,7 +278,7 @@ abstract class EditActivity(private val type: Type) :
add(R.string.pin, R.drawable.pin, MenuItem.SHOW_AS_ACTION_ALWAYS) { pin() } add(R.string.pin, R.drawable.pin, MenuItem.SHOW_AS_ACTION_ALWAYS) { pin() }
bindPinned() bindPinned()
when (model.folder) { when (notallyModel.folder) {
Folder.NOTES -> { Folder.NOTES -> {
add(R.string.delete, R.drawable.delete, MenuItem.SHOW_AS_ACTION_ALWAYS) { add(R.string.delete, R.drawable.delete, MenuItem.SHOW_AS_ACTION_ALWAYS) {
delete() delete()
@ -425,7 +427,7 @@ abstract class EditActivity(private val type: Type) :
} }
protected fun createFolderActions() = protected fun createFolderActions() =
when (model.folder) { when (notallyModel.folder) {
Folder.NOTES -> Folder.NOTES ->
listOf( listOf(
Action(R.string.archive, R.drawable.archive, callback = ::archive), Action(R.string.archive, R.drawable.archive, callback = ::archive),
@ -449,20 +451,26 @@ abstract class EditActivity(private val type: Type) :
open fun setupListeners() { open fun setupListeners() {
binding.EnterTitle.initHistory(changeHistory) { text -> binding.EnterTitle.initHistory(changeHistory) { text ->
model.title = text.trim().toString() notallyModel.title = text.trim().toString()
} }
} }
open fun setStateFromModel() { open fun setStateFromModel() {
val (date, datePrefixResId) = val (date, datePrefixResId) =
when (preferences.notesSorting.value.sortedBy) { when (preferences.notesSorting.value.sortedBy) {
NotesSortBy.CREATION_DATE -> Pair(model.timestamp, R.string.creation_date) NotesSortBy.CREATION_DATE -> Pair(notallyModel.timestamp, R.string.creation_date)
NotesSortBy.MODIFIED_DATE -> Pair(model.modifiedTimestamp, R.string.modified_date) NotesSortBy.MODIFIED_DATE ->
Pair(notallyModel.modifiedTimestamp, R.string.modified_date)
else -> Pair(null, null) else -> Pair(null, null)
} }
binding.Date.displayFormattedTimestamp(date, preferences.dateFormat.value, datePrefixResId) binding.Date.displayFormattedTimestamp(date, preferences.dateFormat.value, datePrefixResId)
binding.EnterTitle.setText(model.title) binding.EnterTitle.setText(notallyModel.title)
Operations.bindLabels(binding.LabelGroup, model.labels, model.textSize, paddingTop = true) Operations.bindLabels(
binding.LabelGroup,
notallyModel.labels,
notallyModel.textSize,
paddingTop = true,
)
setColor() setColor()
} }
@ -475,10 +483,10 @@ abstract class EditActivity(private val type: Type) :
val body = charSequence ?: string val body = charSequence ?: string
if (body != null) { if (body != null) {
model.body = Editable.Factory.getInstance().newEditable(body) notallyModel.body = Editable.Factory.getInstance().newEditable(body)
} }
if (title != null) { if (title != null) {
model.title = title notallyModel.title = title
} }
} }
@ -499,7 +507,7 @@ abstract class EditActivity(private val type: Type) :
} }
private fun startRecordAudioActivity() { private fun startRecordAudioActivity() {
if (model.audioRoot != null) { if (notallyModel.audioRoot != null) {
val intent = Intent(this, RecordAudioActivity::class.java) val intent = Intent(this, RecordAudioActivity::class.java)
recordAudioActivityResultLauncher.launch(intent) recordAudioActivityResultLauncher.launch(intent)
} else showToast(R.string.insert_an_sd_card_audio) } else showToast(R.string.insert_an_sd_card_audio)
@ -518,7 +526,7 @@ abstract class EditActivity(private val type: Type) :
} }
override fun addImages() { override fun addImages() {
if (model.imageRoot != null) { if (notallyModel.imageRoot != null) {
val intent = val intent =
Intent(Intent.ACTION_GET_CONTENT) Intent(Intent.ACTION_GET_CONTENT)
.apply { .apply {
@ -533,7 +541,7 @@ abstract class EditActivity(private val type: Type) :
} }
override fun attachFiles() { override fun attachFiles() {
if (model.filesRoot != null) { if (notallyModel.filesRoot != null) {
val intent = val intent =
Intent(Intent.ACTION_GET_CONTENT) Intent(Intent.ACTION_GET_CONTENT)
.apply { .apply {
@ -549,24 +557,24 @@ abstract class EditActivity(private val type: Type) :
override fun changeColor() { override fun changeColor() {
showColorSelectDialog { selectedColor -> showColorSelectDialog { selectedColor ->
model.color = selectedColor notallyModel.color = selectedColor
setColor() setColor()
} }
} }
override fun changeLabels() { override fun changeLabels() {
val intent = Intent(this, SelectLabelsActivity::class.java) val intent = Intent(this, SelectLabelsActivity::class.java)
intent.putStringArrayListExtra(SelectLabelsActivity.SELECTED_LABELS, model.labels) intent.putStringArrayListExtra(SelectLabelsActivity.SELECTED_LABELS, notallyModel.labels)
selectLabelsActivityResultLauncher.launch(intent) selectLabelsActivityResultLauncher.launch(intent)
} }
override fun share() { override fun share() {
val body = val body =
when (type) { when (type) {
Type.NOTE -> model.body Type.NOTE -> notallyModel.body
Type.LIST -> Operations.getBody(model.items.toMutableList()) Type.LIST -> Operations.getBody(notallyModel.items.toMutableList())
} }
Operations.shareNote(this, model.title, body) Operations.shareNote(this, notallyModel.title, body)
} }
private fun delete() { private fun delete() {
@ -584,11 +592,11 @@ abstract class EditActivity(private val type: Type) :
private fun moveNote(toFolder: Folder) { private fun moveNote(toFolder: Folder) {
val resultIntent = val resultIntent =
Intent().apply { Intent().apply {
putExtra(NOTE_ID, model.id) putExtra(NOTE_ID, notallyModel.id)
putExtra(FOLDER_FROM, model.folder.name) putExtra(FOLDER_FROM, notallyModel.folder.name)
putExtra(FOLDER_TO, toFolder.name) putExtra(FOLDER_TO, toFolder.name)
} }
model.folder = toFolder notallyModel.folder = toFolder
setResult(RESULT_OK, resultIntent) setResult(RESULT_OK, resultIntent)
finish() finish()
} }
@ -598,7 +606,7 @@ abstract class EditActivity(private val type: Type) :
.setMessage(R.string.delete_note_forever) .setMessage(R.string.delete_note_forever)
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
lifecycleScope.launch { lifecycleScope.launch {
model.deleteBaseNote() notallyModel.deleteBaseNote()
super.finish() super.finish()
} }
} }
@ -607,17 +615,17 @@ abstract class EditActivity(private val type: Type) :
} }
fun pin() { fun pin() {
model.pinned = !model.pinned notallyModel.pinned = !notallyModel.pinned
bindPinned() bindPinned()
} }
private fun setupImages() { private fun setupImages() {
val imageAdapter = val imageAdapter =
PreviewImageAdapter(model.imageRoot) { position -> PreviewImageAdapter(notallyModel.imageRoot) { position ->
val intent = val intent =
Intent(this, ViewImageActivity::class.java).apply { Intent(this, ViewImageActivity::class.java).apply {
putExtra(ViewImageActivity.POSITION, position) putExtra(ViewImageActivity.POSITION, position)
putExtra(Constants.SelectedBaseNote, model.id) putExtra(Constants.SelectedBaseNote, notallyModel.id)
} }
viewImagesActivityResultLauncher.launch(intent) viewImagesActivityResultLauncher.launch(intent)
} }
@ -656,7 +664,7 @@ abstract class EditActivity(private val type: Type) :
) )
} }
model.images.observe(this) { list -> notallyModel.images.observe(this) { list ->
imageAdapter.submitList(list) imageAdapter.submitList(list)
binding.ImagePreview.isVisible = list.isNotEmpty() binding.ImagePreview.isVisible = list.isNotEmpty()
binding.ImagePreviewPosition.isVisible = list.size > 1 binding.ImagePreviewPosition.isVisible = list.size > 1
@ -666,13 +674,13 @@ abstract class EditActivity(private val type: Type) :
private fun setupFiles() { private fun setupFiles() {
val fileAdapter = val fileAdapter =
PreviewFileAdapter({ fileAttachment -> PreviewFileAdapter({ fileAttachment ->
if (model.filesRoot == null) { if (notallyModel.filesRoot == null) {
return@PreviewFileAdapter return@PreviewFileAdapter
} }
val intent = val intent =
Intent(Intent.ACTION_VIEW) Intent(Intent.ACTION_VIEW)
.apply { .apply {
val file = File(model.filesRoot, fileAttachment.localName) val file = File(notallyModel.filesRoot, fileAttachment.localName)
val uri = this@EditActivity.getUriForFile(file) val uri = this@EditActivity.getUriForFile(file)
setDataAndType(uri, fileAttachment.mimeType) setDataAndType(uri, fileAttachment.mimeType)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
@ -684,7 +692,7 @@ abstract class EditActivity(private val type: Type) :
.setMessage(getString(R.string.delete_file, fileAttachment.originalName)) .setMessage(getString(R.string.delete_file, fileAttachment.originalName))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
model.deleteFiles(arrayListOf(fileAttachment)) notallyModel.deleteFiles(arrayListOf(fileAttachment))
} }
.show() .show()
return@PreviewFileAdapter true return@PreviewFileAdapter true
@ -696,7 +704,7 @@ abstract class EditActivity(private val type: Type) :
layoutManager = layoutManager =
LinearLayoutManager(this@EditActivity, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(this@EditActivity, LinearLayoutManager.HORIZONTAL, false)
} }
model.files.observe(this) { list -> notallyModel.files.observe(this) { list ->
fileAdapter.submitList(list) fileAdapter.submitList(list)
val visible = list.isNotEmpty() val visible = list.isNotEmpty()
binding.FilesPreview.apply { binding.FilesPreview.apply {
@ -741,7 +749,7 @@ abstract class EditActivity(private val type: Type) :
private fun setupAudios() { private fun setupAudios() {
val adapter = AudioAdapter { position: Int -> val adapter = AudioAdapter { position: Int ->
if (position != -1) { if (position != -1) {
val audio = model.audios.value[position] val audio = notallyModel.audios.value[position]
val intent = Intent(this, PlayAudioActivity::class.java) val intent = Intent(this, PlayAudioActivity::class.java)
intent.putExtra(PlayAudioActivity.AUDIO, audio) intent.putExtra(PlayAudioActivity.AUDIO, audio)
playAudioActivityResultLauncher.launch(intent) playAudioActivityResultLauncher.launch(intent)
@ -749,7 +757,7 @@ abstract class EditActivity(private val type: Type) :
} }
binding.AudioRecyclerView.adapter = adapter binding.AudioRecyclerView.adapter = adapter
model.audios.observe(this) { list -> notallyModel.audios.observe(this) { list ->
adapter.submitList(list) adapter.submitList(list)
binding.AudioHeader.isVisible = list.isNotEmpty() binding.AudioHeader.isVisible = list.isNotEmpty()
binding.AudioRecyclerView.isVisible = list.isNotEmpty() binding.AudioRecyclerView.isVisible = list.isNotEmpty()
@ -757,7 +765,7 @@ abstract class EditActivity(private val type: Type) :
} }
open protected fun setColor() { open protected fun setColor() {
val color = Operations.extractColor(model.color, this) val color = Operations.extractColor(notallyModel.color, this)
binding.ScrollView.apply { binding.ScrollView.apply {
setBackgroundColor(color) setBackgroundColor(color)
setControlsContrastColorForAllViews(color) setControlsContrastColorForAllViews(color)
@ -776,9 +784,9 @@ abstract class EditActivity(private val type: Type) :
} }
} }
val title = model.textSize.editTitleSize val title = notallyModel.textSize.editTitleSize
val date = model.textSize.displayBodySize val date = notallyModel.textSize.displayBodySize
val body = model.textSize.editBodySize val body = notallyModel.textSize.editBodySize
binding.EnterTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, title) binding.EnterTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, title)
binding.Date.setTextSize(TypedValue.COMPLEX_UNIT_SP, date) binding.Date.setTextSize(TypedValue.COMPLEX_UNIT_SP, date)
@ -787,8 +795,8 @@ abstract class EditActivity(private val type: Type) :
setupImages() setupImages()
setupFiles() setupFiles()
setupAudios() setupAudios()
model.addingFiles.setupProgressDialog(this, R.string.adding_files) notallyModel.addingFiles.setupProgressDialog(this, R.string.adding_files)
model.eventBus.observe(this) { event -> notallyModel.eventBus.observe(this) { event ->
event.handle { errors -> displayFileErrors(errors) } event.handle { errors -> displayFileErrors(errors) }
} }
@ -798,7 +806,7 @@ abstract class EditActivity(private val type: Type) :
private fun bindPinned() { private fun bindPinned() {
val icon: Int val icon: Int
val title: Int val title: Int
if (model.pinned) { if (notallyModel.pinned) {
icon = R.drawable.unpin icon = R.drawable.unpin
title = R.string.unpin title = R.string.unpin
} else { } else {

View file

@ -30,12 +30,12 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
private lateinit var listManager: ListManager private lateinit var listManager: ListManager
override fun finish() { override fun finish() {
model.setItems(items.toMutableList()) notallyModel.setItems(items.toMutableList())
super.finish() super.finish()
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
model.setItems(items.toMutableList()) notallyModel.setItems(items.toMutableList())
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@ -92,7 +92,7 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
override fun configureUI() { override fun configureUI() {
binding.EnterTitle.setOnNextAction { listManager.moveFocusToNext(-1) } binding.EnterTitle.setOnNextAction { listManager.moveFocusToNext(-1) }
if (model.isNewNote || model.items.isEmpty()) { if (notallyModel.isNewNote || notallyModel.items.isEmpty()) {
listManager.add(pushChange = false) listManager.add(pushChange = false)
} }
} }
@ -118,8 +118,8 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
} }
adapter = adapter =
ListItemAdapter( ListItemAdapter(
Operations.extractColor(model.color, this), Operations.extractColor(notallyModel.color, this),
model.textSize, notallyModel.textSize,
elevation, elevation,
NotallyXPreferences.getInstance(application), NotallyXPreferences.getInstance(application),
listManager, listManager,
@ -133,7 +133,7 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
if (sortCallback is ListItemSortedByCheckedCallback) { if (sortCallback is ListItemSortedByCheckedCallback) {
sortCallback.setList(items) sortCallback.setList(items)
} }
items.init(model.items) items.init(notallyModel.items)
adapter?.setList(items) adapter?.setList(items)
binding.RecyclerView.adapter = adapter binding.RecyclerView.adapter = adapter
listManager.adapter = adapter!! listManager.adapter = adapter!!
@ -142,6 +142,6 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
override fun setColor() { override fun setColor() {
super.setColor() super.setColor()
adapter?.setBackgroundColor(Operations.extractColor(model.color, this)) adapter?.setBackgroundColor(Operations.extractColor(notallyModel.color, this))
} }
} }

View file

@ -68,7 +68,7 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
setupEditor() setupEditor()
if (model.isNewNote) { if (notallyModel.isNewNote) {
binding.EnterBody.requestFocus() binding.EnterBody.requestFocus()
} }
} }
@ -120,7 +120,7 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
return 0 return 0
} }
searchResultIndices = searchResultIndices =
model.body.toString().findAllOccurrences(search).onEach { (startIdx, endIdx) -> notallyModel.body.toString().findAllOccurrences(search).onEach { (startIdx, endIdx) ->
binding.EnterBody.highlight(startIdx, endIdx, false) binding.EnterBody.highlight(startIdx, endIdx, false)
} }
return searchResultIndices!!.size return searchResultIndices!!.size
@ -136,8 +136,8 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
override fun setupListeners() { override fun setupListeners() {
super.setupListeners() super.setupListeners()
binding.EnterBody.initHistory(changeHistory) { text -> binding.EnterBody.initHistory(changeHistory) { text ->
val textChanged = !model.body.toString().contentEquals(text) val textChanged = !notallyModel.body.toString().contentEquals(text)
model.body = text notallyModel.body = text
if (textChanged && searchResultIndices?.isNotEmpty() == true) { if (textChanged && searchResultIndices?.isNotEmpty() == true) {
val amount = highlightSearchResults(search) val amount = highlightSearchResults(search)
setSearchResultsAmount(amount) setSearchResultsAmount(amount)
@ -151,7 +151,7 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
} }
private fun updateEditText() { private fun updateEditText() {
binding.EnterBody.text = model.body binding.EnterBody.text = notallyModel.body
} }
private fun setupEditor() { private fun setupEditor() {
@ -336,7 +336,9 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
fun linkNote(activityResultLauncher: ActivityResultLauncher<Intent>) { fun linkNote(activityResultLauncher: ActivityResultLauncher<Intent>) {
val intent = val intent =
Intent(this, PickNoteActivity::class.java).apply { putExtra(EXCLUDE_NOTE_ID, model.id) } Intent(this, PickNoteActivity::class.java).apply {
putExtra(EXCLUDE_NOTE_ID, notallyModel.id)
}
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} }

View file

@ -64,7 +64,7 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
val value = binding.EditText.text.toString().trim() val value = binding.EditText.text.toString().trim()
if (value.isNotEmpty()) { if (value.isNotEmpty()) {
val label = Label(value) val label = Label(value)
model.insertLabel(label) { success -> baseModel.insertLabel(label) { success ->
if (success) { if (success) {
dialog.dismiss() dialog.dismiss()
} else showToast(R.string.label_exists) } else showToast(R.string.label_exists)
@ -95,7 +95,7 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
) )
} }
model.labels.observe(this) { labels -> baseModel.labels.observe(this) { labels ->
labelAdapter.submitList(labels) labelAdapter.submitList(labels)
if (labels.isEmpty()) { if (labels.isEmpty()) {
binding.EmptyState.visibility = View.VISIBLE binding.EmptyState.visibility = View.VISIBLE

View file

@ -114,6 +114,8 @@ class NotallyXPreferences private constructor(private val app: Application) {
val iv = ByteArrayPreference("encryption_iv", preferences, null) val iv = ByteArrayPreference("encryption_iv", preferences, null)
val databaseEncryptionKey = val databaseEncryptionKey =
EncryptedPassphrasePreference("database_encryption_key", preferences, ByteArray(0)) EncryptedPassphrasePreference("database_encryption_key", preferences, ByteArray(0))
val fallbackDatabaseEncryptionKey =
ByteArrayPreference("fallback_database_encryption_key", encryptedPreferences, ByteArray(0))
val dataOnExternalStorage = val dataOnExternalStorage =
BooleanPreference("dataOnExternalStorage", preferences, false, R.string.data_on_external) BooleanPreference("dataOnExternalStorage", preferences, false, R.string.data_on_external)

View file

@ -6,6 +6,8 @@ import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.philkes.notallyx.data.NotallyDatabase.Companion.DatabaseName import com.philkes.notallyx.data.NotallyDatabase.Companion.DatabaseName
import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel
import com.philkes.notallyx.presentation.viewmodel.preference.BiometricLock
import java.io.File import java.io.File
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.Cipher import javax.crypto.Cipher
@ -98,7 +100,7 @@ fun getInitializedCipherForDecryption(
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
private fun getCipher(): Cipher { fun getCipher(): Cipher {
return Cipher.getInstance( return Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + KeyProperties.KEY_ALGORITHM_AES +
"/" + "/" +
@ -107,3 +109,14 @@ private fun getCipher(): Cipher {
KeyProperties.ENCRYPTION_PADDING_PKCS7 KeyProperties.ENCRYPTION_PADDING_PKCS7
) )
} }
@RequiresApi(Build.VERSION_CODES.M)
fun Context.disableBiometricLock(model: BaseNoteModel, cipher: Cipher? = null) {
val encryptedPassphrase = model.preferences.databaseEncryptionKey.value
val passphrase =
cipher?.doFinal(encryptedPassphrase)
?: model.preferences.fallbackDatabaseEncryptionKey.value!!
model.closeDatabase()
decryptDatabase(this, passphrase)
model.savePreference(model.preferences.biometricLock, BiometricLock.DISABLED)
}

View file

@ -23,7 +23,7 @@ fun Activity.showBiometricOrPinPrompt(
titleResId: Int, titleResId: Int,
descriptionResId: Int? = null, descriptionResId: Int? = null,
onSuccess: (cipher: Cipher) -> Unit, onSuccess: (cipher: Cipher) -> Unit,
onFailure: () -> Unit, onFailure: (errorCode: Int?) -> Unit,
) { ) {
showBiometricOrPinPrompt( showBiometricOrPinPrompt(
isForDecrypt, isForDecrypt,
@ -44,7 +44,7 @@ fun Fragment.showBiometricOrPinPrompt(
descriptionResId: Int, descriptionResId: Int,
cipherIv: ByteArray? = null, cipherIv: ByteArray? = null,
onSuccess: (cipher: Cipher) -> Unit, onSuccess: (cipher: Cipher) -> Unit,
onFailure: () -> Unit, onFailure: (errorCode: Int?) -> Unit,
) { ) {
showBiometricOrPinPrompt( showBiometricOrPinPrompt(
isForDecrypt, isForDecrypt,
@ -66,23 +66,24 @@ private fun showBiometricOrPinPrompt(
descriptionResId: Int? = null, descriptionResId: Int? = null,
cipherIv: ByteArray? = null, cipherIv: ByteArray? = null,
onSuccess: (cipher: Cipher) -> Unit, onSuccess: (cipher: Cipher) -> Unit,
onFailure: () -> Unit, onFailure: (errorCode: Int?) -> Unit,
) { ) {
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// Android 11+ with BiometricPrompt and Authenticators // Android 11+ with BiometricPrompt and Authenticators
val prompt = BiometricPrompt.Builder(context) val prompt =
.apply { BiometricPrompt.Builder(context)
setTitle(context.getString(titleResId)) .apply {
descriptionResId?.let { setTitle(context.getString(titleResId))
setDescription(context.getString(descriptionResId)) descriptionResId?.let {
} setDescription(context.getString(descriptionResId))
setAllowedAuthenticators( }
BiometricManager.Authenticators.BIOMETRIC_STRONG or setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL BiometricManager.Authenticators.DEVICE_CREDENTIAL
) )
} }
.build() .build()
val cipher = val cipher =
if (isForDecrypt) { if (isForDecrypt) {
getInitializedCipherForDecryption(iv = cipherIv!!) getInitializedCipherForDecryption(iv = cipherIv!!)
@ -103,12 +104,12 @@ private fun showBiometricOrPinPrompt(
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
onFailure.invoke() onFailure.invoke(null)
} }
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
onFailure.invoke() onFailure.invoke(errorCode)
} }
}, },
) )
@ -124,9 +125,9 @@ private fun showBiometricOrPinPrompt(
} }
setNegativeButton( setNegativeButton(
context.getString(R.string.cancel), context.getString(R.string.cancel),
context.mainExecutor context.mainExecutor,
) { _, _ -> ) { _, _ ->
onFailure.invoke() onFailure.invoke(null)
} }
} }
.build() .build()
@ -150,12 +151,12 @@ private fun showBiometricOrPinPrompt(
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
onFailure.invoke() onFailure.invoke(null)
} }
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
onFailure.invoke() onFailure.invoke(errorCode)
} }
}, },
) )
@ -166,7 +167,7 @@ private fun showBiometricOrPinPrompt(
ContextCompat.getSystemService(context, FingerprintManager::class.java) ContextCompat.getSystemService(context, FingerprintManager::class.java)
if ( if (
fingerprintManager?.isHardwareDetected == true && fingerprintManager?.isHardwareDetected == true &&
fingerprintManager.hasEnrolledFingerprints() fingerprintManager.hasEnrolledFingerprints()
) { ) {
val cipher = val cipher =
if (isForDecrypt) { if (isForDecrypt) {
@ -188,7 +189,7 @@ private fun showBiometricOrPinPrompt(
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
onFailure.invoke() onFailure.invoke(null)
} }
override fun onAuthenticationError( override fun onAuthenticationError(
@ -196,7 +197,7 @@ private fun showBiometricOrPinPrompt(
errString: CharSequence?, errString: CharSequence?,
) { ) {
super.onAuthenticationError(errorCode, errString) super.onAuthenticationError(errorCode, errString)
onFailure.invoke() onFailure.invoke(errorCode)
} }
}, },
null, null,
@ -225,7 +226,7 @@ private fun promptPinAuthentication(
context: Context, context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>, activityResultLauncher: ActivityResultLauncher<Intent>,
titleResId: Int, titleResId: Int,
onFailure: () -> Unit, onFailure: (errorCode: Int?) -> Unit,
) { ) {
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java) val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -239,10 +240,10 @@ private fun promptPinAuthentication(
if (intent != null) { if (intent != null) {
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} else { } else {
onFailure.invoke() onFailure.invoke(null)
} }
} else { } else {
onFailure.invoke() onFailure.invoke(null)
} }
} else { } else {
// For API 21-22, use isKeyguardSecure // For API 21-22, use isKeyguardSecure
@ -255,10 +256,10 @@ private fun promptPinAuthentication(
if (intent != null) { if (intent != null) {
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} else { } else {
onFailure.invoke() onFailure.invoke(null)
} }
} else { } else {
onFailure.invoke() onFailure.invoke(null)
} }
} }
} }

View file

@ -76,6 +76,7 @@
<string name="deleting_files">Dateien löschen</string> <string name="deleting_files">Dateien löschen</string>
<string name="deleting_images">Bilder löschen</string> <string name="deleting_images">Bilder löschen</string>
<string name="descending">Absteigend</string> <string name="descending">Absteigend</string>
<string name="disable">Deaktivieren</string>
<string name="disable_auto_backup">Automatisches Backup deaktivieren</string> <string name="disable_auto_backup">Automatisches Backup deaktivieren</string>
<string name="disable_external_data">Verschiebe die Daten zurück in den internen Speicher</string> <string name="disable_external_data">Verschiebe die Daten zurück in den internen Speicher</string>
<string name="disable_lock_description">Dies entschlüsselt außerdem die Datenbank</string> <string name="disable_lock_description">Dies entschlüsselt außerdem die Datenbank</string>
@ -222,6 +223,7 @@
<string name="unknown_error">Unbekannter Fehler</string> <string name="unknown_error">Unbekannter Fehler</string>
<string name="unknown_name">Unbekannter Name</string> <string name="unknown_name">Unbekannter Name</string>
<string name="unlock">Entsperre mittels Biometrie/PIN</string> <string name="unlock">Entsperre mittels Biometrie/PIN</string>
<string name="unlock_with_biometrics_not_setup">Die biometrische Sperre ist aktiviert, allerdings ist für dein Gerät keine Biometrie/PIN mehr eingerichtet.\n\nUm die biometrische zu deaktivieren klicke Deaktivieren, ansonsten richte für dein Geräte Biometrie/PIN ein</string>
<string name="unpin">Loslösen</string> <string name="unpin">Loslösen</string>
<string name="updated_link">Link aktualisiert</string> <string name="updated_link">Link aktualisiert</string>
<string name="view">Ansicht</string> <string name="view">Ansicht</string>

View file

@ -90,6 +90,7 @@
<string name="deleting_files">Deleting files</string> <string name="deleting_files">Deleting files</string>
<string name="deleting_images">Deleting images</string> <string name="deleting_images">Deleting images</string>
<string name="descending">Descending</string> <string name="descending">Descending</string>
<string name="disable">Disable</string>
<string name="disable_auto_backup">Disable auto backup</string> <string name="disable_auto_backup">Disable auto backup</string>
<string name="disable_external_data">Move data back to internal storage</string> <string name="disable_external_data">Move data back to internal storage</string>
<string name="disable_lock_description">This will also decrypt the database</string> <string name="disable_lock_description">This will also decrypt the database</string>
@ -258,6 +259,7 @@
<string name="unknown_error">Unknown error</string> <string name="unknown_error">Unknown error</string>
<string name="unknown_name">Unknown name</string> <string name="unknown_name">Unknown name</string>
<string name="unlock">Unlock via Biometric/PIN</string> <string name="unlock">Unlock via Biometric/PIN</string>
<string name="unlock_with_biometrics_not_setup">You have previously enabled biometric lock but Biometrics/PIN are not setup for your device anymore.\n\nIf you wish to disable biometric lock press Disable, otherwise setup Biometrics/PIN for your device</string>
<string name="unpin">Unpin</string> <string name="unpin">Unpin</string>
<string name="updated_link">Updated Link</string> <string name="updated_link">Updated Link</string>
<string name="view">View</string> <string name="view">View</string>

Binary file not shown.