diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt index a87f8b43..e77b469d 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt @@ -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 : AppCompatActivity() { @@ -26,6 +34,7 @@ abstract class LockedActivity : 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 : 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() + } } } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt index 123d48bc..2f024769 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/MainActivity.kt @@ -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() { private lateinit var exportFileActivityResultLauncher: ActivityResultLauncher private lateinit var exportNotesActivityResultLauncher: ActivityResultLauncher - 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() { .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() { } 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, maxLabelsToDisplay: Int) { @@ -218,7 +219,7 @@ class MainActivity : LockedActivity() { } 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() { 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() { } 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() { 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() { 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() { 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() { 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() { 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() { 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) } } } } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt index 2a75c2dd..422bb1f6 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt @@ -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) }, diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt index 65ac9a49..3a4a6d17 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditActivity.kt @@ -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 = 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 { diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt index 0b4370e1..06371d2a 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditListActivity.kt @@ -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)) } } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt index b05a7d02..59206842 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/EditNoteActivity.kt @@ -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) { 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) } diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/SelectLabelsActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/SelectLabelsActivity.kt index ca966261..a1b1b83e 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/note/SelectLabelsActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/note/SelectLabelsActivity.kt @@ -64,7 +64,7 @@ class SelectLabelsActivity : LockedActivity() { 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() { ) } - model.labels.observe(this) { labels -> + baseModel.labels.observe(this) { labels -> labelAdapter.submitList(labels) if (labels.isEmpty()) { binding.EmptyState.visibility = View.VISIBLE diff --git a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt index 7d664205..02cee866 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/viewmodel/preference/NotallyXPreferences.kt @@ -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) diff --git a/app/src/main/java/com/philkes/notallyx/utils/security/EncryptionUtils.kt b/app/src/main/java/com/philkes/notallyx/utils/security/EncryptionUtils.kt index 76b447d1..ae58161f 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/security/EncryptionUtils.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/security/EncryptionUtils.kt @@ -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) +} diff --git a/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt b/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt index a9e0fa0b..ff14d4fc 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt @@ -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, 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) } } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 502342be..c28fd1e0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -76,6 +76,7 @@ Dateien löschen Bilder löschen Absteigend + Deaktivieren Automatisches Backup deaktivieren Verschiebe die Daten zurück in den internen Speicher Dies entschlüsselt außerdem die Datenbank @@ -222,6 +223,7 @@ Unbekannter Fehler Unbekannter Name Entsperre mittels Biometrie/PIN + 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 Loslösen Link aktualisiert Ansicht diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a79628a6..18f530aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -90,6 +90,7 @@ Deleting files Deleting images Descending + Disable Disable auto backup Move data back to internal storage This will also decrypt the database @@ -258,6 +259,7 @@ Unknown error Unknown name Unlock via Biometric/PIN + 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 Unpin Updated Link View diff --git a/translations/translations.xlsx b/translations/translations.xlsx index fcaaea5e..5fecd286 100644 Binary files a/translations/translations.xlsx and b/translations/translations.xlsx differ