Skip to content
Merged
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 @@ -15,6 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philkes.notallyx.NotallyXApplication
Expand All @@ -23,6 +24,7 @@ import com.philkes.notallyx.presentation.showToast
import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
import kotlinx.coroutines.launch

abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {

Expand Down Expand Up @@ -82,9 +84,12 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
.setMessage(R.string.unlock_with_biometrics_not_setup)
.setPositiveButton(R.string.disable) { _, _ ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
baseModel.disableBiometricLock()
lifecycleScope.launch {
baseModel.disableBiometricLock()
showToast(R.string.biometrics_disable_success)
}
}
show()
hide()
}
.setNegativeButton(R.string.tap_to_set_up) { _, _ ->
val intent =
Expand All @@ -102,8 +107,10 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {

BIOMETRIC_ERROR_HW_NOT_PRESENT -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
baseModel.disableBiometricLock()
showToast(R.string.biometrics_disable_success)
lifecycleScope.launch {
baseModel.disableBiometricLock()
showToast(R.string.biometrics_disable_success)
}
}
show()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE
import com.philkes.notallyx.NotallyXApplication
Expand Down Expand Up @@ -58,11 +59,16 @@ import com.philkes.notallyx.utils.getExtraBooleanFromBundleOrIntent
import com.philkes.notallyx.utils.getLastExceptionLog
import com.philkes.notallyx.utils.getLogFile
import com.philkes.notallyx.utils.getUriForFile
import com.philkes.notallyx.utils.log
import com.philkes.notallyx.utils.reportBug
import com.philkes.notallyx.utils.security.DecryptionException
import com.philkes.notallyx.utils.security.EncryptionException
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
import com.philkes.notallyx.utils.showErrorDialog
import com.philkes.notallyx.utils.viewLogs
import com.philkes.notallyx.utils.wrapWithChooser
import java.util.Date
import kotlinx.coroutines.launch

class SettingsFragment : Fragment() {

Expand Down Expand Up @@ -663,7 +669,11 @@ class SettingsFragment : Fragment() {
R.string.reset_settings_message,
R.string.reset_settings,
{ _, _ ->
model.resetPreferences { _ -> showToast(R.string.reset_settings_success) }
lifecycleScope.launch {
model.resetPreferences { _ ->
showToast(R.string.reset_settings_success)
}
}
},
)
}
Expand Down Expand Up @@ -839,12 +849,27 @@ class SettingsFragment : Fragment() {
R.string.enable_lock_title,
R.string.enable_lock_description,
onSuccess = { cipher ->
val app = (requireActivity().application as NotallyXApplication)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
model.enableBiometricLock(cipher)
lifecycleScope.launch {
try {
model.enableBiometricLock(cipher)
} catch (e: EncryptionException) {
app.log(TAG, throwable = e)
showErrorDialog(
e,
R.string.biometrics_setup_failure,
getString(
R.string.biometrics_setup_failure_encrypt,
getString(R.string.report_bug),
),
)
return@launch
}
app.locked.value = false
showToast(R.string.biometrics_setup_success)
}
}
val app = (activity?.application as NotallyXApplication)
app.locked.value = false
showToast(R.string.biometrics_setup_success)
},
) {
showBiometricsNotSetupDialog()
Expand All @@ -860,9 +885,25 @@ class SettingsFragment : Fragment() {
model.preferences.iv.value!!,
onSuccess = { cipher ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
model.disableBiometricLock(cipher)
val app = (requireActivity().application as NotallyXApplication)
lifecycleScope.launch {
try {
model.disableBiometricLock(cipher)
} catch (e: DecryptionException) {
app.log(TAG, throwable = e)
showErrorDialog(
e,
R.string.biometrics_setup_failure,
getString(
R.string.biometrics_setup_failure_decrypt,
getString(R.string.report_bug),
),
)
return@launch
}
showToast(R.string.biometrics_disable_success)
}
}
showToast(R.string.biometrics_disable_success)
},
) {}
}
Expand Down Expand Up @@ -906,6 +947,7 @@ class SettingsFragment : Fragment() {
}

companion object {
private const val TAG = "SettingsFragment"
const val EXTRA_SHOW_IMPORT_BACKUPS_FOLDER =
"notallyx.intent.extra.SHOW_IMPORT_BACKUPS_FOLDER"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.room.withTransaction
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philkes.notallyx.R
import com.philkes.notallyx.data.NotallyDatabase
import com.philkes.notallyx.data.NotallyDatabase.Companion.DATABASE_NAME
import com.philkes.notallyx.data.dao.BaseNoteDao
import com.philkes.notallyx.data.dao.CommonDao
import com.philkes.notallyx.data.dao.LabelDao
Expand Down Expand Up @@ -60,6 +61,7 @@ import com.philkes.notallyx.utils.Cache
import com.philkes.notallyx.utils.MIME_TYPE_JSON
import com.philkes.notallyx.utils.backup.clearAllFolders
import com.philkes.notallyx.utils.backup.clearAllLabels
import com.philkes.notallyx.utils.backup.copyDatabase
import com.philkes.notallyx.utils.backup.exportAsZip
import com.philkes.notallyx.utils.backup.exportPdfFile
import com.philkes.notallyx.utils.backup.exportPlainTextFile
Expand All @@ -71,10 +73,15 @@ import com.philkes.notallyx.utils.cancelNoteReminders
import com.philkes.notallyx.utils.deleteAttachments
import com.philkes.notallyx.utils.getBackupDir
import com.philkes.notallyx.utils.getExternalImagesDirectory
import com.philkes.notallyx.utils.getExternalMediaDirectory
import com.philkes.notallyx.utils.log
import com.philkes.notallyx.utils.scheduleNoteReminders
import com.philkes.notallyx.utils.security.DecryptionException
import com.philkes.notallyx.utils.security.EncryptionException
import com.philkes.notallyx.utils.security.decryptDatabase
import com.philkes.notallyx.utils.security.encryptDatabase
import com.philkes.notallyx.utils.security.isEncryptedDatabase
import com.philkes.notallyx.utils.security.isUnencryptedDatabase
import com.philkes.notallyx.utils.toReadablePath
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
Expand Down Expand Up @@ -283,24 +290,55 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
}
}

fun enableBiometricLock(cipher: Cipher) {
suspend fun enableBiometricLock(cipher: Cipher) {
savePreference(preferences.iv, cipher.iv)
val passphrase = preferences.databaseEncryptionKey.init(cipher)
encryptDatabase(app, passphrase)
savePreference(preferences.fallbackDatabaseEncryptionKey, passphrase)
savePreference(preferences.biometricLock, BiometricLock.ENABLED)
withContext(Dispatchers.IO) {
database.close()
val (_, dbFileCopy) = app.copyDatabase(suffix = "-encrypt")
val (_, dbFileBackup) = app.copyDatabase(suffix = "-encrypt-backup")
encryptDatabase(app, dbFileCopy, passphrase)
val originalDbFile = NotallyDatabase.getCurrentDatabaseFile(app)
dbFileCopy.copyTo(originalDbFile, overwrite = true)
if (originalDbFile.isUnencryptedDatabase) {
dbFileBackup.copyTo(originalDbFile, overwrite = true)
val externalBackupFile =
File(app.getExternalMediaDirectory(), "${DATABASE_NAME}_Backup-encrypt")
dbFileBackup.copyTo(externalBackupFile, overwrite = true)
throw EncryptionException(
"Encrypt succeeded but overwritten database is not encrypted"
)
}
savePreference(preferences.fallbackDatabaseEncryptionKey, passphrase)
savePreference(preferences.biometricLock, BiometricLock.ENABLED)
}
}

@RequiresApi(Build.VERSION_CODES.M)
fun disableBiometricLock(cipher: Cipher? = null, callback: (() -> Unit)? = null) {
suspend fun disableBiometricLock(cipher: Cipher? = null, callback: (() -> Unit)? = null) {
val encryptedPassphrase = preferences.databaseEncryptionKey.value
val passphrase =
cipher?.doFinal(encryptedPassphrase)
?: preferences.fallbackDatabaseEncryptionKey.value!!
database.close()
decryptDatabase(app, passphrase)
savePreference(preferences.biometricLock, BiometricLock.DISABLED)
callback?.invoke()
withContext(Dispatchers.IO) {
database.close()
val (_, dbFileCopy) = app.copyDatabase(decrypt = false, suffix = "-decrypt")
val (_, dbFileBackup) = app.copyDatabase(decrypt = false, suffix = "-decrypt-backup")
decryptDatabase(app, dbFileCopy, passphrase)
val originalDbFile = NotallyDatabase.getCurrentDatabaseFile(app)
dbFileCopy.copyTo(originalDbFile, overwrite = true)
if (originalDbFile.isEncryptedDatabase) {
dbFileBackup.copyTo(originalDbFile, overwrite = true)
val externalBackupFile =
File(app.getExternalMediaDirectory(), "${DATABASE_NAME}_Backup-decrypt")
dbFileBackup.copyTo(externalBackupFile, overwrite = true)
throw DecryptionException(
"Decrypt succeeded but overwritten database is still encrypted"
)
}
savePreference(preferences.biometricLock, BiometricLock.DISABLED)
callback?.invoke()
}
}

fun <T> savePreference(preference: BasePreference<T>, value: T) {
Expand Down Expand Up @@ -605,7 +643,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
}
}

fun resetPreferences(callback: (restartRequired: Boolean) -> Unit) {
suspend fun resetPreferences(callback: (restartRequired: Boolean) -> Unit) {
val backupsFolder = preferences.backupsFolder.value
val publicFolder = preferences.dataInPublicFolder.value
val isThemeDefault = preferences.theme.value == Theme.FOLLOW_SYSTEM
Expand Down
49 changes: 49 additions & 0 deletions app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.philkes.notallyx.R
import com.philkes.notallyx.data.model.BaseNote
import com.philkes.notallyx.data.model.Type
import com.philkes.notallyx.data.model.toText
import com.philkes.notallyx.databinding.DialogErrorBinding
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.EXTRA_SELECTED_BASE_NOTE
import com.philkes.notallyx.presentation.activity.note.EditListActivity
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
Expand Down Expand Up @@ -182,6 +183,54 @@ fun ContextWrapper.viewLogs() {
fun ContextWrapper.getLogFileUri() =
getLogFile().let { if (it.exists()) getUriForFile(it) else null }

fun Fragment.showErrorDialog(
throwable: Throwable,
titleResId: Int,
message: String,
originalStacktrace: String? = null,
) {
val stacktrace = throwable.stackTraceToString()
val layout =
DialogErrorBinding.inflate(layoutInflater, null, false).apply {
ExceptionTitle.text = message
ExceptionDetails.text = stacktrace
CopyButton.setOnClickListener { requireContext().copyToClipBoard(stacktrace) }
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(titleResId)
.setView(layout.root)
.setPositiveButton(R.string.report_bug) { dialog, _ ->
dialog.cancel()
reportBug(originalStacktrace ?: throwable.stackTraceToString())
}
.setCancelButton()
.show()
}

fun Activity.showErrorDialog(
throwable: Throwable,
titleResId: Int,
message: String,
originalStacktrace: String? = null,
) {
val stacktrace = throwable.stackTraceToString()
val layout =
DialogErrorBinding.inflate(layoutInflater, null, false).apply {
ExceptionTitle.text = message
ExceptionDetails.text = stacktrace
CopyButton.setOnClickListener { copyToClipBoard(stacktrace) }
}
MaterialAlertDialogBuilder(this)
.setTitle(titleResId)
.setView(layout.root)
.setPositiveButton(R.string.report_bug) { dialog, _ ->
dialog.cancel()
reportBug(originalStacktrace ?: throwable.stackTraceToString())
}
.setCancelButton()
.show()
}

private const val MAX_LOGS_FILE_SIZE_KB: Long = 2048

private fun Context.logToFile(
Expand Down
37 changes: 9 additions & 28 deletions app/src/main/java/com/philkes/notallyx/utils/ErrorActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import androidx.lifecycle.lifecycleScope
import cat.ereza.customactivityoncrash.CustomActivityOnCrash
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philkes.notallyx.R
import com.philkes.notallyx.R.string.auto_backup_failed
import com.philkes.notallyx.R.string.crash_export_backup_failed
import com.philkes.notallyx.R.string.report_bug
import com.philkes.notallyx.databinding.ActivityErrorBinding
import com.philkes.notallyx.databinding.DialogErrorBinding
import com.philkes.notallyx.presentation.getQuantityString
import com.philkes.notallyx.presentation.setCancelButton
import com.philkes.notallyx.presentation.setupProgressDialog
Expand Down Expand Up @@ -92,13 +94,12 @@ class ErrorActivity : AppCompatActivity() {
result.data?.data?.let { uri ->
val preferences = NotallyXPreferences.getInstance(this)
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
// MaterialAlertDialogBuilder(this)
// .setTitle(R.string.auto_backup_failed)
//
// .setMessage(throwable.stackTraceToString())
// .setCancelButton()
// .show()
showErrorDialog(throwable, stacktrace)
showErrorDialog(
throwable,
auto_backup_failed,
getString(crash_export_backup_failed, this.getString(report_bug)),
originalStacktrace = stacktrace,
)
}
Comment on lines +97 to 103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Import showErrorDialog to fix the unresolved reference.

showErrorDialog(...) lives in com.philkes.notallyx.presentation. Without the import this file won’t compile.

 import com.philkes.notallyx.presentation.setupProgressDialog
 import com.philkes.notallyx.presentation.showToast
+import com.philkes.notallyx.presentation.showErrorDialog

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/com/philkes/notallyx/utils/ErrorActivity.kt around lines 97
to 103, the call to showErrorDialog is an unresolved reference because the
function is defined in com.philkes.notallyx.presentation; add the missing import
by adding import com.philkes.notallyx.presentation.showErrorDialog near the
other imports at the top of the file so the call compiles, then re-run build to
confirm resolution.

lifecycleScope.launch(exceptionHandler) {
val exportedNotes =
Expand All @@ -123,26 +124,6 @@ class ErrorActivity : AppCompatActivity() {
exportBackupProgress.setupProgressDialog(this)
}

private fun showErrorDialog(throwable: Throwable, originalStacktrace: String?) {
val stacktrace = throwable.stackTraceToString()
val layout =
DialogErrorBinding.inflate(layoutInflater, null, false).apply {
ExceptionTitle.text =
getString(R.string.crash_export_backup_failed, getString(R.string.report_bug))
ExceptionDetails.text = stacktrace
CopyButton.setOnClickListener { copyToClipBoard(stacktrace) }
}
MaterialAlertDialogBuilder(this)
.setTitle(R.string.auto_backup_failed)
.setView(layout.root)
.setPositiveButton(R.string.report_bug) { dialog, _ ->
dialog.cancel()
reportBug(originalStacktrace)
}
.setCancelButton()
.show()
}

companion object {
private const val TAG = "ErrorActivity"
}
Expand Down
Loading