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.KeyguardManager
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.Bundle
import android.provider.Settings
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philkes.notallyx.NotallyXApplication
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.NotallyXPreferences
import com.philkes.notallyx.utils.security.disableBiometricLock
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
@ -26,6 +34,7 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: T
protected lateinit var preferences: NotallyXPreferences
protected val baseModel: BaseNoteModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -68,8 +77,41 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
biometricAuthenticationActivityResultLauncher,
R.string.unlock,
onSuccess = { unlock() },
) {
finish()
) { errorCode ->
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.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.widget.doAfterTextChanged
@ -77,11 +76,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
private lateinit var exportFileActivityResultLauncher: ActivityResultLauncher<Intent>
private lateinit var exportNotesActivityResultLauncher: ActivityResultLauncher<Intent>
private val model: BaseNoteModel by viewModels()
private val actionModeCancelCallback =
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
model.actionMode.close(true)
baseModel.actionMode.close(true)
}
}
@ -159,10 +157,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
.setCheckable(true)
.setIcon(R.drawable.settings)
}
model.preferences.labelsHiddenInNavigation.observe(this) { hiddenLabels ->
hideLabelsInNavigation(hiddenLabels, model.preferences.maxLabels.value)
baseModel.preferences.labelsHiddenInNavigation.observe(this) { hiddenLabels ->
hideLabelsInNavigation(hiddenLabels, baseModel.preferences.maxLabels.value)
}
model.preferences.maxLabels.observe(this) { maxLabels ->
baseModel.preferences.maxLabels.observe(this) { maxLabels ->
binding.NavigationView.menu.setupLabelsMenuItems(labels, maxLabels)
}
}
@ -201,7 +199,10 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
} else null
configuration = AppBarConfiguration(binding.NavigationView.menu, binding.DrawerLayout)
setupActionBarWithNavController(navController, configuration)
hideLabelsInNavigation(model.preferences.labelsHiddenInNavigation.value, maxLabelsToDisplay)
hideLabelsInNavigation(
baseModel.preferences.labelsHiddenInNavigation.value,
maxLabelsToDisplay,
)
}
private fun hideLabelsInNavigation(hiddenLabels: Set<String>, maxLabelsToDisplay: Int) {
@ -218,7 +219,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
}
private fun setupActionMode() {
binding.ActionMode.setNavigationOnClickListener { model.actionMode.close(true) }
binding.ActionMode.setNavigationOnClickListener { baseModel.actionMode.close(true) }
val transition =
MaterialFade().apply {
@ -230,7 +231,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
excludeTarget(binding.NavigationView, true)
}
model.actionMode.enabled.observe(this) { enabled ->
baseModel.actionMode.enabled.observe(this) { enabled ->
TransitionManager.beginDelayedTransition(binding.RelativeLayout, transition)
if (enabled) {
binding.Toolbar.visibility = View.GONE
@ -245,23 +246,23 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
}
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) {
val folderFrom = model.actionMode.getFirstNote().folder
val ids = model.moveBaseNotes(folderTo)
val folderFrom = baseModel.actionMode.getFirstNote().folder
val ids = baseModel.moveBaseNotes(folderTo)
Snackbar.make(
findViewById(R.id.DrawerLayout),
getQuantityString(folderTo.movedToResId(), ids.size),
Snackbar.LENGTH_SHORT,
)
.apply { setAction(R.string.undo) { model.moveBaseNotes(ids, folderFrom) } }
.apply { setAction(R.string.undo) { baseModel.moveBaseNotes(ids, folderFrom) } }
.show()
}
private fun share() {
val baseNote = model.actionMode.getFirstNote()
val baseNote = baseModel.actionMode.getFirstNote()
val body =
when (baseNote.type) {
Type.NOTE -> baseNote.body.applySpans(baseNote.spans)
@ -273,19 +274,19 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
private fun deleteForever() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.delete_selected_notes)
.setPositiveButton(R.string.delete) { _, _ -> model.deleteSelectedBaseNotes() }
.setPositiveButton(R.string.delete) { _, _ -> baseModel.deleteSelectedBaseNotes() }
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun label() {
val baseNotes = model.actionMode.selectedNotes.values
val baseNotes = baseModel.actionMode.selectedNotes.values
lifecycleScope.launch {
val labels = model.getAllLabels()
val labels = baseModel.getAllLabels()
if (labels.isNotEmpty()) {
displaySelectLabelsDialog(labels, baseNotes)
} else {
model.actionMode.close(true)
baseModel.actionMode.close(true)
navigateWithAnimation(R.id.Labels)
}
}
@ -340,15 +341,15 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
noteLabels
}
baseNotes.zip(updatedBaseNotesLabels).forEach { (baseNote, updatedLabels) ->
model.updateBaseNoteLabels(updatedLabels, baseNote.id)
baseModel.updateBaseNoteLabels(updatedLabels, baseNote.id)
}
}
.show()
}
private fun exportSelectedNotes(mimeType: ExportMimeType) {
if (model.actionMode.count.value == 1) {
val baseNote = model.actionMode.getFirstNote()
if (baseModel.actionMode.count.value == 1) {
val baseNote = baseModel.actionMode.getFirstNote()
when (mimeType) {
ExportMimeType.PDF -> {
exportPdfFile(
@ -392,7 +393,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.apply { addCategory(Intent.CATEGORY_DEFAULT) }
.wrapWithChooser(this@MainActivity)
model.selectedExportMimeType = mimeType
baseModel.selectedExportMimeType = mimeType
exportNotesActivityResultLauncher.launch(intent)
}
}
@ -425,7 +426,7 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
putExtra(Intent.EXTRA_TITLE, file.nameWithoutExtension!!)
}
.wrapWithChooser(this@MainActivity)
model.selectedExportFile = file
baseModel.selectedExportFile = file
exportFileActivityResultLauncher.launch(intent)
}
@ -521,22 +522,26 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
private fun setupSearch() {
binding.EnterSearchKeyword.apply {
setText(model.keyword)
setText(baseModel.keyword)
doAfterTextChanged { text ->
model.keyword = requireNotNull(text).trim().toString()
baseModel.keyword = requireNotNull(text).trim().toString()
if (
model.keyword.isNotEmpty() &&
baseModel.keyword.isNotEmpty() &&
navController.currentDestination?.id != R.id.Search
) {
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)
}
}
setOnFocusChangeListener { v, hasFocus ->
if (hasFocus && navController.currentDestination?.id != R.id.Search) {
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)
}
}
@ -547,13 +552,13 @@ class MainActivity : LockedActivity<ActivityMainBinding>() {
exportFileActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> model.exportSelectedFileToUri(uri) }
result.data?.data?.let { uri -> baseModel.exportSelectedFileToUri(uri) }
}
}
exportNotesActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
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.catchNoBrowserInstalled
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.showBiometricOrPinPrompt
import com.philkes.notallyx.utils.wrapWithChooser
@ -618,6 +618,10 @@ class SettingsFragment : Fragment() {
model.savePreference(model.preferences.iv, cipher.iv)
val passphrase = model.preferences.databaseEncryptionKey.init(cipher)
encryptDatabase(requireContext(), passphrase)
model.savePreference(
model.preferences.fallbackDatabaseEncryptionKey,
passphrase,
)
model.savePreference(model.preferences.biometricLock, BiometricLock.ENABLED)
}
val app = (activity?.application as NotallyXApplication)
@ -638,11 +642,7 @@ class SettingsFragment : Fragment() {
model.preferences.iv.value!!,
onSuccess = { cipher ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val encryptedPassphrase = model.preferences.databaseEncryptionKey.value
val passphrase = cipher.doFinal(encryptedPassphrase)
model.closeDatabase()
decryptDatabase(requireContext(), passphrase)
model.savePreference(model.preferences.biometricLock, BiometricLock.DISABLED)
requireContext().disableBiometricLock(model, cipher)
}
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 searchResultsAmount = NotNullLiveData(-1)
internal val model: NotallyModel by viewModels()
internal val notallyModel: NotallyModel by viewModels()
internal lateinit var changeHistory: ChangeHistory
protected val undos: MutableList<View> = mutableListOf()
@ -93,9 +93,9 @@ abstract class EditActivity(private val type: Type) :
override fun finish() {
lifecycleScope.launch(Dispatchers.Main) {
if (model.isEmpty()) {
model.deleteBaseNote()
} else if (model.isModified()) {
if (notallyModel.isEmpty()) {
notallyModel.deleteBaseNote()
} else if (notallyModel.isModified()) {
saveNote()
}
super.finish()
@ -104,21 +104,21 @@ abstract class EditActivity(private val type: Type) :
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong("id", model.id)
if (model.isModified()) {
outState.putLong("id", notallyModel.id)
if (notallyModel.isModified()) {
lifecycleScope.launch { saveNote() }
}
}
open suspend fun saveNote() {
model.modifiedTimestamp = System.currentTimeMillis()
model.saveNote()
WidgetProvider.sendBroadcast(application, longArrayOf(model.id))
notallyModel.modifiedTimestamp = System.currentTimeMillis()
notallyModel.saveNote()
WidgetProvider.sendBroadcast(application, longArrayOf(notallyModel.id))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model.type = type
notallyModel.type = type
initialiseBinding()
setContentView(binding.root)
@ -126,12 +126,14 @@ abstract class EditActivity(private val type: Type) :
val persistedId = savedInstanceState?.getLong("id")
val selectedId = intent.getLongExtra(Constants.SelectedBaseNote, 0L)
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()
} else if (model.isNewNote) {
intent.getStringExtra(Constants.SelectedLabel)?.let { model.setLabels(listOf(it)) }
} else if (notallyModel.isNewNote) {
intent.getStringExtra(Constants.SelectedLabel)?.let {
notallyModel.setLabels(listOf(it))
}
}
setupToolbars()
@ -152,7 +154,7 @@ abstract class EditActivity(private val type: Type) :
recordAudioActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
model.addAudio()
notallyModel.addAudio()
}
}
addImagesActivityResultLauncher =
@ -162,11 +164,11 @@ abstract class EditActivity(private val type: Type) :
val clipData = result.data?.clipData
if (uri != null) {
val uris = arrayOf(uri)
model.addImages(uris)
notallyModel.addImages(uris)
} else if (clipData != null) {
val uris =
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()) {
model.deleteImages(list)
notallyModel.deleteImages(list)
}
}
}
@ -191,12 +193,12 @@ abstract class EditActivity(private val type: Type) :
if (result.resultCode == RESULT_OK) {
val list =
result.data?.getStringArrayListExtra(SelectLabelsActivity.SELECTED_LABELS)
if (list != null && list != model.labels) {
model.setLabels(list)
if (list != null && list != notallyModel.labels) {
notallyModel.setLabels(list)
Operations.bindLabels(
binding.LabelGroup,
model.labels,
model.textSize,
notallyModel.labels,
notallyModel.textSize,
paddingTop = true,
)
}
@ -214,7 +216,7 @@ abstract class EditActivity(private val type: Type) :
)
}
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
if (uri != null) {
val uris = arrayOf(uri)
model.addFiles(uris)
notallyModel.addFiles(uris)
} else if (clipData != null) {
val uris =
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() }
bindPinned()
when (model.folder) {
when (notallyModel.folder) {
Folder.NOTES -> {
add(R.string.delete, R.drawable.delete, MenuItem.SHOW_AS_ACTION_ALWAYS) {
delete()
@ -425,7 +427,7 @@ abstract class EditActivity(private val type: Type) :
}
protected fun createFolderActions() =
when (model.folder) {
when (notallyModel.folder) {
Folder.NOTES ->
listOf(
Action(R.string.archive, R.drawable.archive, callback = ::archive),
@ -449,20 +451,26 @@ abstract class EditActivity(private val type: Type) :
open fun setupListeners() {
binding.EnterTitle.initHistory(changeHistory) { text ->
model.title = text.trim().toString()
notallyModel.title = text.trim().toString()
}
}
open fun setStateFromModel() {
val (date, datePrefixResId) =
when (preferences.notesSorting.value.sortedBy) {
NotesSortBy.CREATION_DATE -> Pair(model.timestamp, R.string.creation_date)
NotesSortBy.MODIFIED_DATE -> Pair(model.modifiedTimestamp, R.string.modified_date)
NotesSortBy.CREATION_DATE -> Pair(notallyModel.timestamp, R.string.creation_date)
NotesSortBy.MODIFIED_DATE ->
Pair(notallyModel.modifiedTimestamp, R.string.modified_date)
else -> Pair(null, null)
}
binding.Date.displayFormattedTimestamp(date, preferences.dateFormat.value, datePrefixResId)
binding.EnterTitle.setText(model.title)
Operations.bindLabels(binding.LabelGroup, model.labels, model.textSize, paddingTop = true)
binding.EnterTitle.setText(notallyModel.title)
Operations.bindLabels(
binding.LabelGroup,
notallyModel.labels,
notallyModel.textSize,
paddingTop = true,
)
setColor()
}
@ -475,10 +483,10 @@ abstract class EditActivity(private val type: Type) :
val body = charSequence ?: string
if (body != null) {
model.body = Editable.Factory.getInstance().newEditable(body)
notallyModel.body = Editable.Factory.getInstance().newEditable(body)
}
if (title != null) {
model.title = title
notallyModel.title = title
}
}
@ -499,7 +507,7 @@ abstract class EditActivity(private val type: Type) :
}
private fun startRecordAudioActivity() {
if (model.audioRoot != null) {
if (notallyModel.audioRoot != null) {
val intent = Intent(this, RecordAudioActivity::class.java)
recordAudioActivityResultLauncher.launch(intent)
} else showToast(R.string.insert_an_sd_card_audio)
@ -518,7 +526,7 @@ abstract class EditActivity(private val type: Type) :
}
override fun addImages() {
if (model.imageRoot != null) {
if (notallyModel.imageRoot != null) {
val intent =
Intent(Intent.ACTION_GET_CONTENT)
.apply {
@ -533,7 +541,7 @@ abstract class EditActivity(private val type: Type) :
}
override fun attachFiles() {
if (model.filesRoot != null) {
if (notallyModel.filesRoot != null) {
val intent =
Intent(Intent.ACTION_GET_CONTENT)
.apply {
@ -549,24 +557,24 @@ abstract class EditActivity(private val type: Type) :
override fun changeColor() {
showColorSelectDialog { selectedColor ->
model.color = selectedColor
notallyModel.color = selectedColor
setColor()
}
}
override fun changeLabels() {
val intent = Intent(this, SelectLabelsActivity::class.java)
intent.putStringArrayListExtra(SelectLabelsActivity.SELECTED_LABELS, model.labels)
intent.putStringArrayListExtra(SelectLabelsActivity.SELECTED_LABELS, notallyModel.labels)
selectLabelsActivityResultLauncher.launch(intent)
}
override fun share() {
val body =
when (type) {
Type.NOTE -> model.body
Type.LIST -> Operations.getBody(model.items.toMutableList())
Type.NOTE -> notallyModel.body
Type.LIST -> Operations.getBody(notallyModel.items.toMutableList())
}
Operations.shareNote(this, model.title, body)
Operations.shareNote(this, notallyModel.title, body)
}
private fun delete() {
@ -584,11 +592,11 @@ abstract class EditActivity(private val type: Type) :
private fun moveNote(toFolder: Folder) {
val resultIntent =
Intent().apply {
putExtra(NOTE_ID, model.id)
putExtra(FOLDER_FROM, model.folder.name)
putExtra(NOTE_ID, notallyModel.id)
putExtra(FOLDER_FROM, notallyModel.folder.name)
putExtra(FOLDER_TO, toFolder.name)
}
model.folder = toFolder
notallyModel.folder = toFolder
setResult(RESULT_OK, resultIntent)
finish()
}
@ -598,7 +606,7 @@ abstract class EditActivity(private val type: Type) :
.setMessage(R.string.delete_note_forever)
.setPositiveButton(R.string.delete) { _, _ ->
lifecycleScope.launch {
model.deleteBaseNote()
notallyModel.deleteBaseNote()
super.finish()
}
}
@ -607,17 +615,17 @@ abstract class EditActivity(private val type: Type) :
}
fun pin() {
model.pinned = !model.pinned
notallyModel.pinned = !notallyModel.pinned
bindPinned()
}
private fun setupImages() {
val imageAdapter =
PreviewImageAdapter(model.imageRoot) { position ->
PreviewImageAdapter(notallyModel.imageRoot) { position ->
val intent =
Intent(this, ViewImageActivity::class.java).apply {
putExtra(ViewImageActivity.POSITION, position)
putExtra(Constants.SelectedBaseNote, model.id)
putExtra(Constants.SelectedBaseNote, notallyModel.id)
}
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)
binding.ImagePreview.isVisible = list.isNotEmpty()
binding.ImagePreviewPosition.isVisible = list.size > 1
@ -666,13 +674,13 @@ abstract class EditActivity(private val type: Type) :
private fun setupFiles() {
val fileAdapter =
PreviewFileAdapter({ fileAttachment ->
if (model.filesRoot == null) {
if (notallyModel.filesRoot == null) {
return@PreviewFileAdapter
}
val intent =
Intent(Intent.ACTION_VIEW)
.apply {
val file = File(model.filesRoot, fileAttachment.localName)
val file = File(notallyModel.filesRoot, fileAttachment.localName)
val uri = this@EditActivity.getUriForFile(file)
setDataAndType(uri, fileAttachment.mimeType)
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))
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete) { _, _ ->
model.deleteFiles(arrayListOf(fileAttachment))
notallyModel.deleteFiles(arrayListOf(fileAttachment))
}
.show()
return@PreviewFileAdapter true
@ -696,7 +704,7 @@ abstract class EditActivity(private val type: Type) :
layoutManager =
LinearLayoutManager(this@EditActivity, LinearLayoutManager.HORIZONTAL, false)
}
model.files.observe(this) { list ->
notallyModel.files.observe(this) { list ->
fileAdapter.submitList(list)
val visible = list.isNotEmpty()
binding.FilesPreview.apply {
@ -741,7 +749,7 @@ abstract class EditActivity(private val type: Type) :
private fun setupAudios() {
val adapter = AudioAdapter { position: Int ->
if (position != -1) {
val audio = model.audios.value[position]
val audio = notallyModel.audios.value[position]
val intent = Intent(this, PlayAudioActivity::class.java)
intent.putExtra(PlayAudioActivity.AUDIO, audio)
playAudioActivityResultLauncher.launch(intent)
@ -749,7 +757,7 @@ abstract class EditActivity(private val type: Type) :
}
binding.AudioRecyclerView.adapter = adapter
model.audios.observe(this) { list ->
notallyModel.audios.observe(this) { list ->
adapter.submitList(list)
binding.AudioHeader.isVisible = list.isNotEmpty()
binding.AudioRecyclerView.isVisible = list.isNotEmpty()
@ -757,7 +765,7 @@ abstract class EditActivity(private val type: Type) :
}
open protected fun setColor() {
val color = Operations.extractColor(model.color, this)
val color = Operations.extractColor(notallyModel.color, this)
binding.ScrollView.apply {
setBackgroundColor(color)
setControlsContrastColorForAllViews(color)
@ -776,9 +784,9 @@ abstract class EditActivity(private val type: Type) :
}
}
val title = model.textSize.editTitleSize
val date = model.textSize.displayBodySize
val body = model.textSize.editBodySize
val title = notallyModel.textSize.editTitleSize
val date = notallyModel.textSize.displayBodySize
val body = notallyModel.textSize.editBodySize
binding.EnterTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, title)
binding.Date.setTextSize(TypedValue.COMPLEX_UNIT_SP, date)
@ -787,8 +795,8 @@ abstract class EditActivity(private val type: Type) :
setupImages()
setupFiles()
setupAudios()
model.addingFiles.setupProgressDialog(this, R.string.adding_files)
model.eventBus.observe(this) { event ->
notallyModel.addingFiles.setupProgressDialog(this, R.string.adding_files)
notallyModel.eventBus.observe(this) { event ->
event.handle { errors -> displayFileErrors(errors) }
}
@ -798,7 +806,7 @@ abstract class EditActivity(private val type: Type) :
private fun bindPinned() {
val icon: Int
val title: Int
if (model.pinned) {
if (notallyModel.pinned) {
icon = R.drawable.unpin
title = R.string.unpin
} else {

View file

@ -30,12 +30,12 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
private lateinit var listManager: ListManager
override fun finish() {
model.setItems(items.toMutableList())
notallyModel.setItems(items.toMutableList())
super.finish()
}
override fun onSaveInstanceState(outState: Bundle) {
model.setItems(items.toMutableList())
notallyModel.setItems(items.toMutableList())
super.onSaveInstanceState(outState)
}
@ -92,7 +92,7 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
override fun configureUI() {
binding.EnterTitle.setOnNextAction { listManager.moveFocusToNext(-1) }
if (model.isNewNote || model.items.isEmpty()) {
if (notallyModel.isNewNote || notallyModel.items.isEmpty()) {
listManager.add(pushChange = false)
}
}
@ -118,8 +118,8 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
}
adapter =
ListItemAdapter(
Operations.extractColor(model.color, this),
model.textSize,
Operations.extractColor(notallyModel.color, this),
notallyModel.textSize,
elevation,
NotallyXPreferences.getInstance(application),
listManager,
@ -133,7 +133,7 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
if (sortCallback is ListItemSortedByCheckedCallback) {
sortCallback.setList(items)
}
items.init(model.items)
items.init(notallyModel.items)
adapter?.setList(items)
binding.RecyclerView.adapter = adapter
listManager.adapter = adapter!!
@ -142,6 +142,6 @@ class EditListActivity : EditActivity(Type.LIST), MoreListActions {
override fun 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()
if (model.isNewNote) {
if (notallyModel.isNewNote) {
binding.EnterBody.requestFocus()
}
}
@ -120,7 +120,7 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
return 0
}
searchResultIndices =
model.body.toString().findAllOccurrences(search).onEach { (startIdx, endIdx) ->
notallyModel.body.toString().findAllOccurrences(search).onEach { (startIdx, endIdx) ->
binding.EnterBody.highlight(startIdx, endIdx, false)
}
return searchResultIndices!!.size
@ -136,8 +136,8 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
override fun setupListeners() {
super.setupListeners()
binding.EnterBody.initHistory(changeHistory) { text ->
val textChanged = !model.body.toString().contentEquals(text)
model.body = text
val textChanged = !notallyModel.body.toString().contentEquals(text)
notallyModel.body = text
if (textChanged && searchResultIndices?.isNotEmpty() == true) {
val amount = highlightSearchResults(search)
setSearchResultsAmount(amount)
@ -151,7 +151,7 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
}
private fun updateEditText() {
binding.EnterBody.text = model.body
binding.EnterBody.text = notallyModel.body
}
private fun setupEditor() {
@ -336,7 +336,9 @@ class EditNoteActivity : EditActivity(Type.NOTE), AddNoteActions {
fun linkNote(activityResultLauncher: ActivityResultLauncher<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)
}

View file

@ -64,7 +64,7 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
val value = binding.EditText.text.toString().trim()
if (value.isNotEmpty()) {
val label = Label(value)
model.insertLabel(label) { success ->
baseModel.insertLabel(label) { success ->
if (success) {
dialog.dismiss()
} 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)
if (labels.isEmpty()) {
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 databaseEncryptionKey =
EncryptedPassphrasePreference("database_encryption_key", preferences, ByteArray(0))
val fallbackDatabaseEncryptionKey =
ByteArrayPreference("fallback_database_encryption_key", encryptedPreferences, ByteArray(0))
val dataOnExternalStorage =
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 androidx.annotation.RequiresApi
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.security.KeyStore
import javax.crypto.Cipher
@ -98,7 +100,7 @@ fun getInitializedCipherForDecryption(
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getCipher(): Cipher {
fun getCipher(): Cipher {
return Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES +
"/" +
@ -107,3 +109,14 @@ private fun getCipher(): Cipher {
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,
descriptionResId: Int? = null,
onSuccess: (cipher: Cipher) -> Unit,
onFailure: () -> Unit,
onFailure: (errorCode: Int?) -> Unit,
) {
showBiometricOrPinPrompt(
isForDecrypt,
@ -44,7 +44,7 @@ fun Fragment.showBiometricOrPinPrompt(
descriptionResId: Int,
cipherIv: ByteArray? = null,
onSuccess: (cipher: Cipher) -> Unit,
onFailure: () -> Unit,
onFailure: (errorCode: Int?) -> Unit,
) {
showBiometricOrPinPrompt(
isForDecrypt,
@ -66,23 +66,24 @@ private fun showBiometricOrPinPrompt(
descriptionResId: Int? = null,
cipherIv: ByteArray? = null,
onSuccess: (cipher: Cipher) -> Unit,
onFailure: () -> Unit,
onFailure: (errorCode: Int?) -> Unit,
) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// Android 11+ with BiometricPrompt and Authenticators
val prompt = BiometricPrompt.Builder(context)
.apply {
setTitle(context.getString(titleResId))
descriptionResId?.let {
setDescription(context.getString(descriptionResId))
}
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
val prompt =
BiometricPrompt.Builder(context)
.apply {
setTitle(context.getString(titleResId))
descriptionResId?.let {
setDescription(context.getString(descriptionResId))
}
setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
}
.build()
)
}
.build()
val cipher =
if (isForDecrypt) {
getInitializedCipherForDecryption(iv = cipherIv!!)
@ -103,12 +104,12 @@ private fun showBiometricOrPinPrompt(
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke()
onFailure.invoke(null)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke()
onFailure.invoke(errorCode)
}
},
)
@ -124,9 +125,9 @@ private fun showBiometricOrPinPrompt(
}
setNegativeButton(
context.getString(R.string.cancel),
context.mainExecutor
context.mainExecutor,
) { _, _ ->
onFailure.invoke()
onFailure.invoke(null)
}
}
.build()
@ -150,12 +151,12 @@ private fun showBiometricOrPinPrompt(
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke()
onFailure.invoke(null)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke()
onFailure.invoke(errorCode)
}
},
)
@ -166,7 +167,7 @@ private fun showBiometricOrPinPrompt(
ContextCompat.getSystemService(context, FingerprintManager::class.java)
if (
fingerprintManager?.isHardwareDetected == true &&
fingerprintManager.hasEnrolledFingerprints()
fingerprintManager.hasEnrolledFingerprints()
) {
val cipher =
if (isForDecrypt) {
@ -188,7 +189,7 @@ private fun showBiometricOrPinPrompt(
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure.invoke()
onFailure.invoke(null)
}
override fun onAuthenticationError(
@ -196,7 +197,7 @@ private fun showBiometricOrPinPrompt(
errString: CharSequence?,
) {
super.onAuthenticationError(errorCode, errString)
onFailure.invoke()
onFailure.invoke(errorCode)
}
},
null,
@ -225,7 +226,7 @@ private fun promptPinAuthentication(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
titleResId: Int,
onFailure: () -> Unit,
onFailure: (errorCode: Int?) -> Unit,
) {
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -239,10 +240,10 @@ private fun promptPinAuthentication(
if (intent != null) {
activityResultLauncher.launch(intent)
} else {
onFailure.invoke()
onFailure.invoke(null)
}
} else {
onFailure.invoke()
onFailure.invoke(null)
}
} else {
// For API 21-22, use isKeyguardSecure
@ -255,10 +256,10 @@ private fun promptPinAuthentication(
if (intent != null) {
activityResultLauncher.launch(intent)
} else {
onFailure.invoke()
onFailure.invoke(null)
}
} else {
onFailure.invoke()
onFailure.invoke(null)
}
}
}

View file

@ -76,6 +76,7 @@
<string name="deleting_files">Dateien löschen</string>
<string name="deleting_images">Bilder löschen</string>
<string name="descending">Absteigend</string>
<string name="disable">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_lock_description">Dies entschlüsselt außerdem die Datenbank</string>
@ -222,6 +223,7 @@
<string name="unknown_error">Unbekannter Fehler</string>
<string name="unknown_name">Unbekannter Name</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="updated_link">Link aktualisiert</string>
<string name="view">Ansicht</string>

View file

@ -90,6 +90,7 @@
<string name="deleting_files">Deleting files</string>
<string name="deleting_images">Deleting images</string>
<string name="descending">Descending</string>
<string name="disable">Disable</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_lock_description">This will also decrypt the database</string>
@ -258,6 +259,7 @@
<string name="unknown_error">Unknown error</string>
<string name="unknown_name">Unknown name</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="updated_link">Updated Link</string>
<string name="view">View</string>

Binary file not shown.