Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle user un-enrolling device biometrics while biometric lock is enabled #238

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
Expand Down Expand Up @@ -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()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
},
Expand Down
Loading