From 1f062cb17a986a59dd1c45082dbc5a55b913852b Mon Sep 17 00:00:00 2001 From: Michael Totschnig Date: Tue, 22 Oct 2024 21:45:08 +0200 Subject: [PATCH] sqlcipher migration We can only run tests with encrypted database from command line, where db is not cleared after each test. Hence we add cleanup to some tests (WIP) Add helper to call delete with workaround for https://github.com/sqlcipher/sqlcipher-android/issues/50 --- gradle/libs.versions.toml | 4 +- .../test/espresso/CategoriesCabTest.kt | 40 +++++++++---- .../test/espresso/CriterionReachedTest.kt | 38 ++++++------ .../espresso/CriterionReachedTestTransfer.kt | 20 ++++--- .../test/espresso/DistributionTest.kt | 40 +++++++++++-- .../test/provider/TransactionDebtTest.kt | 7 +-- .../myexpenses/testutils/BaseUiTest.kt | 11 ++-- .../myexpenses/db2/RepositoryBudget.kt | 7 ++- .../myexpenses/db2/RepositoryTransaction.kt | 5 ++ .../org/totschnig/myexpenses/di/DataModule.kt | 4 +- .../provider/BaseTransactionProvider.kt | 2 +- .../myexpenses/provider/MoreDbUtils.kt | 5 +- .../provider/TransactionDatabase.java | 2 +- .../provider/TransactionProvider.java | 60 +++++++++---------- .../sqlcrypt/SQLiteOpenHelperFactory.kt | 16 ++--- 15 files changed, 160 insertions(+), 101 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e120f5daa..5175d2110d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -95,7 +95,7 @@ securityCrypto = "1.1.0-alpha06" simpledialogfragments = "09a642bc42" slf4jApi = "2.0.13" snakeyaml = "2.2" -sqlcipher = "4.5.4" +sqlcipher = "4.6.1" taptargetview = "1.13.3" tesseract4androidOpenmp = "4.7.0" mlkitTextRecognition = "16.0.1" @@ -111,7 +111,7 @@ accompanist-themeadapter-material3 = { module = "com.google.accompanist:accompan acra-core = { module = "ch.acra:acra-core", version.ref = "acraVersion" } acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acraVersion" } acra-mail = { module = "ch.acra:acra-mail", version.ref = "acraVersion" } -android-database-sqlcipher = { module = "net.zetetic:android-database-sqlcipher", version.ref = "sqlcipher" } +android-database-sqlcipher = { module = "net.zetetic:sqlcipher-android", version.ref = "sqlcipher" } android-image-cropper = { module = "com.github.mtotschnig:Android-Image-Cropper", version.ref = "androidImageCropper" } android-state = { module = "com.evernote:android-state", version.ref = "evernote" } android-state-processor = { module = "com.evernote:android-state-processor", version.ref = "evernote" } diff --git a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CategoriesCabTest.kt b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CategoriesCabTest.kt index f467f5f25f..8c4efdbfc3 100644 --- a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CategoriesCabTest.kt +++ b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CategoriesCabTest.kt @@ -29,6 +29,7 @@ import com.adevinta.android.barista.internal.matcher.HelperMatchers import com.google.common.truth.Truth.assertThat import org.hamcrest.CoreMatchers.containsString import org.hamcrest.Matchers +import org.junit.After import org.junit.Test import org.totschnig.myexpenses.R import org.totschnig.myexpenses.activity.Action @@ -37,6 +38,10 @@ import org.totschnig.myexpenses.compose.TEST_TAG_EDIT_TEXT import org.totschnig.myexpenses.compose.TEST_TAG_POSITIVE_BUTTON import org.totschnig.myexpenses.contract.TransactionsContract.Transactions import org.totschnig.myexpenses.db2.FLAG_NEUTRAL +import org.totschnig.myexpenses.db2.deleteAccount +import org.totschnig.myexpenses.db2.deleteBudget +import org.totschnig.myexpenses.db2.deleteCategory +import org.totschnig.myexpenses.db2.deleteTemplate import org.totschnig.myexpenses.model.CurrencyUnit import org.totschnig.myexpenses.model.Grouping import org.totschnig.myexpenses.model.Money @@ -55,14 +60,22 @@ class CategoriesCabTest : BaseComposeTest() { private lateinit var account: org.totschnig.myexpenses.model2.Account private var categoryId: Long = 0 private var origListSize = 0 + private var controlCategory: Long = 0 private fun baseFixture() { account = buildAccount("Test account 1") categoryId = writeCategory(label = "TestCategory") - writeCategory(label = "Control Category") + controlCategory = writeCategory(label = "Control Category") origListSize = repository.count(TransactionProvider.CATEGORIES_URI) } + @After + fun clearDb() { + repository.deleteAccount(account.id) + repository.deleteCategory(categoryId) + repository.deleteCategory(controlCategory) + } + private fun launch() = ActivityScenario.launch( Intent(targetContext, ManageCategories::class.java).also { @@ -73,18 +86,18 @@ class CategoriesCabTest : BaseComposeTest() { testScenario = it } - private fun fixtureWithMappedTransaction() { + private fun fixtureWithMappedTransaction(): Long { baseFixture() - with(Transaction.getNewInstance(account.id, homeCurrency)) { + return with(Transaction.getNewInstance(account.id, homeCurrency)) { amount = Money(homeCurrency, -1200L) catId = categoryId - save(contentResolver) + ContentUris.parseId(save(contentResolver)!!) } } - private fun fixtureWithMappedTemplate() { + private fun fixtureWithMappedTemplate(): Long { baseFixture() - with( + return with( Template( contentResolver, account.id, @@ -95,11 +108,11 @@ class CategoriesCabTest : BaseComposeTest() { ) { amount = Money(CurrencyUnit(Currency.getInstance("USD")), -1200L) catId = categoryId - save(contentResolver) + ContentUris.parseId(save(contentResolver)!!) } } - private fun fixtureWithMappedBudget() { + private fun fixtureWithMappedBudget(): Long { baseFixture() val budget = Budget( 0L, @@ -121,6 +134,7 @@ class CategoriesCabTest : BaseComposeTest() { )!! ) setCategoryBudget(budgetId, categoryId, 50000) + return budgetId } private fun setCategoryBudget( @@ -161,8 +175,9 @@ class CategoriesCabTest : BaseComposeTest() { @Test fun shouldNotDeleteCategoryMappedToTransaction() { - fixtureWithMappedTransaction() + val transactionId = fixtureWithMappedTransaction() launch().use { + assertTextAtPosition("TestCategory", 0) callDelete() onView(withId(com.google.android.material.R.id.snackbar_text)) .check( @@ -176,11 +191,12 @@ class CategoriesCabTest : BaseComposeTest() { ) assertThat(repository.count(TransactionProvider.CATEGORIES_URI)).isEqualTo(origListSize) } + repository.deleteTransaction(transactionId) } @Test fun shouldNotDeleteCategoryMappedToTemplate() { - fixtureWithMappedTemplate() + val templateId = fixtureWithMappedTemplate() launch().use { callDelete() onView(withId(com.google.android.material.R.id.snackbar_text)) @@ -194,12 +210,13 @@ class CategoriesCabTest : BaseComposeTest() { ) ) assertThat(repository.count(TransactionProvider.CATEGORIES_URI)).isEqualTo(origListSize) + repository.deleteTemplate(templateId) } } @Test fun shouldNotDeleteCategoryMappedToBudget() { - fixtureWithMappedBudget() + val budgetId = fixtureWithMappedBudget() launch().use { callDelete(false) onView(withText(containsString(getString(R.string.warning_delete_category_with_budget)))).check( @@ -208,6 +225,7 @@ class CategoriesCabTest : BaseComposeTest() { onView(withText(R.string.response_no)).perform(click()) assertThat(repository.count(TransactionProvider.CATEGORIES_URI)).isEqualTo(origListSize) } + repository.deleteBudget(budgetId) } private fun callDelete(withConfirmation: Boolean = true, position: Int = 0) { diff --git a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTest.kt b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTest.kt index c038177df0..8989717bf9 100644 --- a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTest.kt +++ b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTest.kt @@ -3,22 +3,16 @@ package org.totschnig.myexpenses.test.espresso import android.content.ContentUris import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.onNodeWithText -import androidx.test.espresso.Espresso.onData -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.matcher.ViewMatchers.withId -import org.hamcrest.CoreMatchers.allOf -import org.hamcrest.CoreMatchers.instanceOf +import org.junit.After import org.totschnig.myexpenses.R import org.totschnig.myexpenses.activity.ExpenseEdit -import org.totschnig.myexpenses.adapter.IdHolder import org.totschnig.myexpenses.contract.TransactionsContract.Transactions +import org.totschnig.myexpenses.db2.deleteAccount import org.totschnig.myexpenses.model.CurrencyUnit import org.totschnig.myexpenses.model.Money import org.totschnig.myexpenses.model.Transaction import org.totschnig.myexpenses.model2.Account import org.totschnig.myexpenses.provider.DatabaseConstants -import org.totschnig.myexpenses.testutils.withAccount import java.util.Currency import kotlin.math.absoluteValue import kotlin.math.sign @@ -27,22 +21,18 @@ import kotlin.test.Test class CriterionReachedTest : BaseExpenseEditTest() { val currency = CurrencyUnit(Currency.getInstance("USD")) - lateinit var account2: Account - - fun fixture(criterion: Long, withAccount2: Boolean, openingBalance: Long = 0) { + fun fixture(criterion: Long, openingBalance: Long = 0) { account1 = Account( label = "Test label 1", currency = currency.code, criterion = criterion, openingBalance = openingBalance ).createIn(repository) - if (withAccount2) { - account2 = Account( - label = "Test label 2", - currency = currency.code, - criterion = criterion - ).createIn(repository) - } + } + + @After + fun cleanup() { + repository.deleteAccount(account1.id) } @Test @@ -184,7 +174,7 @@ class CriterionReachedTest : BaseExpenseEditTest() { expectedTitle: Int?, openingBalance: Long = 0, ) { - fixture(criterion, false, openingBalance) + fixture(criterion, openingBalance) launchForResult(intentForNewTransaction.apply { putExtra(ExpenseEdit.KEY_INCOME, amount > 0) putExtra(Transactions.OPERATION_TYPE, Transactions.TYPE_TRANSACTION) @@ -205,7 +195,7 @@ class CriterionReachedTest : BaseExpenseEditTest() { editedAmount: Int, expectedTitle: Int?, ) { - fixture(criterion, false) + fixture(criterion) val transactionId = Transaction.getNewInstance(account1.id, currency).let { it.amount = Money(currency, existingAmount) ContentUris.parseId(it.save(contentResolver)!!) @@ -233,7 +223,12 @@ class CriterionReachedTest : BaseExpenseEditTest() { editedAmount: Int, expectedTitle: Int?, ) { - fixture(criterion, true) + fixture(criterion) + val account2 = Account( + label = "Test label 2", + currency = currency.code, + criterion = criterion + ).createIn(repository) val transactionId = Transaction.getNewInstance(account1.id, currency).let { it.amount = Money(currency, existingAmount) ContentUris.parseId(it.save(contentResolver)!!) @@ -254,5 +249,6 @@ class CriterionReachedTest : BaseExpenseEditTest() { assertFinishing() } } + repository.deleteAccount(account2.id) } } \ No newline at end of file diff --git a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTestTransfer.kt b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTestTransfer.kt index f5248c53e5..1f9a550bcf 100644 --- a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTestTransfer.kt +++ b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/CriterionReachedTestTransfer.kt @@ -9,8 +9,10 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText +import org.junit.After import org.totschnig.myexpenses.R import org.totschnig.myexpenses.contract.TransactionsContract.Transactions +import org.totschnig.myexpenses.db2.deleteAccount import org.totschnig.myexpenses.model.CurrencyUnit import org.totschnig.myexpenses.model.Money import org.totschnig.myexpenses.model.Transfer @@ -31,13 +33,17 @@ class CriterionReachedTestTransfer : BaseExpenseEditTest() { currency = currency.code, criterion = -10000 ).createIn(repository) - if (true) { - account2 = Account( - label = "Saving", - currency = currency.code, - criterion = 5000 - ).createIn(repository) - } + account2 = Account( + label = "Saving", + currency = currency.code, + criterion = 5000 + ).createIn(repository) + } + + @After + fun cleanup() { + repository.deleteAccount(account1.id) + repository.deleteAccount(account2.id) } diff --git a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/DistributionTest.kt b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/DistributionTest.kt index 8d046a3f9a..a3a834b57b 100644 --- a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/DistributionTest.kt +++ b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/espresso/DistributionTest.kt @@ -1,5 +1,6 @@ package org.totschnig.myexpenses.test.espresso +import android.content.ContentUris import android.content.Intent import androidx.annotation.StringRes import androidx.compose.ui.test.junit4.createEmptyComposeRule @@ -17,12 +18,15 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import org.assertj.core.api.Assertions.assertThat import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString +import org.junit.After import org.junit.Rule import org.junit.Test import org.totschnig.myexpenses.R import org.totschnig.myexpenses.activity.DistributionActivity import org.totschnig.myexpenses.activity.ProtectedFragmentActivity import org.totschnig.myexpenses.db2.FLAG_INCOME +import org.totschnig.myexpenses.db2.deleteAccount +import org.totschnig.myexpenses.db2.deleteCategory import org.totschnig.myexpenses.model.Money import org.totschnig.myexpenses.model.Transaction import org.totschnig.myexpenses.model2.Account @@ -36,6 +40,12 @@ class DistributionTest : BaseUiTest() { private lateinit var account: Account + private var categoryExpenseId = 0L + private var categoryIncomeId = 0L + private var transactionExpenseId = 0L + private var transactionIncomeId = 0L + + private fun baseFixture( showIncome: Boolean = false, showExpense: Boolean = true, @@ -56,21 +66,39 @@ class DistributionTest : BaseUiTest() { showExpense: Boolean = true ) { baseFixture(showIncome, showExpense) { - val categoryExpenseId = writeCategory("Expense") - val categoryIncomeId = writeCategory("Income", type = FLAG_INCOME) - with(Transaction.getNewInstance(account.id, homeCurrency)) { + categoryExpenseId = writeCategory("Expense") + categoryIncomeId = writeCategory("Income", type = FLAG_INCOME) + transactionExpenseId = with(Transaction.getNewInstance(account.id, homeCurrency)) { amount = Money(homeCurrency, -1200L) catId = categoryExpenseId - save(contentResolver) + ContentUris.parseId(save(contentResolver)!!) } - with(Transaction.getNewInstance(account.id, homeCurrency)) { + transactionIncomeId = with(Transaction.getNewInstance(account.id, homeCurrency)) { amount = Money(homeCurrency, 3400L) catId = categoryIncomeId - save(contentResolver) + ContentUris.parseId(save(contentResolver)!!) } } } + @After + fun cleanup() { + if (transactionExpenseId != 0L) { + repository.deleteTransaction(transactionExpenseId) + } + if (transactionIncomeId != 0L) { + repository.deleteTransaction(transactionIncomeId) + } + if (categoryExpenseId != 0L) { + repository.deleteCategory(categoryExpenseId) + } + if (categoryIncomeId != 0L) { + repository.deleteCategory(categoryIncomeId) + } + repository.deleteAccount(account.id) + } + + private fun assertIncome() { onView(allOf(withText(containsString("Income")), withText(containsString("34")))) .inRoot(isDialog()) diff --git a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/provider/TransactionDebtTest.kt b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/provider/TransactionDebtTest.kt index f1e2f9be16..397147540c 100644 --- a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/provider/TransactionDebtTest.kt +++ b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/test/provider/TransactionDebtTest.kt @@ -14,7 +14,6 @@ import org.totschnig.myexpenses.provider.insert import org.totschnig.myexpenses.provider.update import org.totschnig.myexpenses.testutils.BaseDbTest import org.totschnig.myexpenses.viewmodel.data.Debt -import java.util.* class TransactionDebtTest: BaseDbTest() { private var testAccountId: Long = 0 @@ -74,7 +73,7 @@ class TransactionDebtTest: BaseDbTest() { "$KEY_ROWID = ?", arrayOf(closedTransaction.toString()) ) kotlin.test.fail("Update of closed debt did not raise SQLiteConstraintException") - } catch (e: SQLiteConstraintException) { + } catch (_: SQLiteConstraintException) { //Expected } } @@ -86,7 +85,7 @@ class TransactionDebtTest: BaseDbTest() { "$KEY_ROWID = ?", arrayOf(closedTransaction.toString()) ) kotlin.test.fail("Delete of transaction for closed debt did not raise SQLiteConstraintException") - } catch (e: SQLiteConstraintException) { + } catch (_: SQLiteConstraintException) { //Expected } } @@ -106,7 +105,7 @@ class TransactionDebtTest: BaseDbTest() { testTransaction.contentValues ) kotlin.test.fail("Insert into closed debt dit no raise SQLiteConstraintException") - } catch (e: SQLiteConstraintException) { + } catch (_: SQLiteConstraintException) { //Expected } } diff --git a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/testutils/BaseUiTest.kt b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/testutils/BaseUiTest.kt index 2ceaef8230..82b643a8ee 100644 --- a/myExpenses/src/androidTest/java/org/totschnig/myexpenses/testutils/BaseUiTest.kt +++ b/myExpenses/src/androidTest/java/org/totschnig/myexpenses/testutils/BaseUiTest.kt @@ -13,7 +13,6 @@ import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.* import androidx.test.espresso.NoMatchingViewException -import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.assertion.ViewAssertions.doesNotExist @@ -129,10 +128,10 @@ abstract class BaseUiTest { ) { inRoot(RootMatchers.isPlatformPopup()) } - }.perform(ViewActions.click()) + }.perform(click()) } catch (_: NoMatchingViewException) { Espresso.openActionBarOverflowMenu(isCab) - onData(menuIdMatcher(menuItemId)).inRoot(RootMatchers.isPlatformPopup()).perform(ViewActions.click()) + onData(menuIdMatcher(menuItemId)).inRoot(RootMatchers.isPlatformPopup()).perform(click()) } } @@ -169,14 +168,14 @@ abstract class BaseUiTest { if (DistributionHelper.isPlay) { try { //without play service a billing setup error dialog is displayed - onView(ViewMatchers.withText(android.R.string.ok)).perform(ViewActions.click()) + onView(ViewMatchers.withText(android.R.string.ok)).perform(click()) } catch (_: Exception) { } } onView(ViewMatchers.withSubstring(getString(R.string.dialog_title_contrib_feature))).check( matches(isDisplayed()) ) - onView(ViewMatchers.withText(R.string.button_try)).perform(ViewActions.scrollTo(), ViewActions.click()) + onView(ViewMatchers.withText(R.string.button_try)).perform(scrollTo(), click()) } } @@ -268,7 +267,7 @@ abstract class BaseUiTest { } fun clickFab() { - onView(withId(R.id.fab)).perform(ViewActions.click()) + onView(withId(R.id.fab)).perform(click()) } fun checkAccount(label: String) { diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryBudget.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryBudget.kt index 505145fd8b..4dc0af134c 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryBudget.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryBudget.kt @@ -31,7 +31,6 @@ import org.totschnig.myexpenses.viewmodel.data.BudgetAllocation import org.totschnig.myexpenses.viewmodel.data.BudgetProgress import org.totschnig.myexpenses.viewmodel.data.DateInfo import org.totschnig.myexpenses.viewmodel.data.DateInfoExtra -import timber.log.Timber import java.lang.IllegalArgumentException import java.time.LocalDate import java.time.temporal.ChronoUnit @@ -239,4 +238,8 @@ suspend fun Repository.loadBudgetProgress(budgetId: Long, period: Pair totalDays, currentDay ) - } \ No newline at end of file + } + +fun Repository.deleteBudget(id: Long) { + contentResolver.delete(ContentUris.withAppendedId(TransactionProvider.BUDGETS_URI, id), null, null) +} \ No newline at end of file diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryTransaction.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryTransaction.kt index 6f128c26e1..8f77ba8643 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryTransaction.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/db2/RepositoryTransaction.kt @@ -247,3 +247,8 @@ private fun ContentResolver.findBySelection( )?.use { if (it.moveToFirst()) it.getLong(0) else null } ?: -1 + + +fun Repository.deleteTemplate(id: Long) { + contentResolver.delete(ContentUris.withAppendedId(TransactionProvider.TEMPLATES_URI, id), null, null) +} \ No newline at end of file diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/di/DataModule.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/di/DataModule.kt index 8ff3e0b4bc..16a58342a6 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/di/DataModule.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/di/DataModule.kt @@ -39,7 +39,7 @@ open class DataModule(private val shouldInsertDefaultTransferCategory: Boolean = companion object { val cryptProvider: SqlCryptProvider by lazy { Class.forName("org.totschnig.sqlcrypt.SQLiteOpenHelperFactory") - .newInstance() as SqlCryptProvider + .getDeclaredConstructor().newInstance() as SqlCryptProvider } } @@ -123,7 +123,7 @@ open class DataModule(private val shouldInsertDefaultTransferCategory: Boolean = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY).use { if (!try { it.isDatabaseIntegrityOk - } catch (e: SQLiteDatabaseCorruptException) { + } catch (_: SQLiteDatabaseCorruptException) { false } ) { diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/BaseTransactionProvider.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/BaseTransactionProvider.kt index a6b5d7e7a5..8bc090c9d9 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/BaseTransactionProvider.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/BaseTransactionProvider.kt @@ -1703,7 +1703,7 @@ abstract class BaseTransactionProvider : ContentProvider() { ) null else category.id } - } catch (e: SQLiteConstraintException) { + } catch (_: SQLiteConstraintException) { null } } diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/MoreDbUtils.kt b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/MoreDbUtils.kt index 58e398c181..31f6ed4208 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/MoreDbUtils.kt +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/MoreDbUtils.kt @@ -597,12 +597,15 @@ fun SupportSQLiteDatabase.update( values: ContentValues, whereClause: String?, whereArgs: Array? -) = //https://github.com/sqlcipher/android-database-sqlcipher/issues/615 +) = //https://github.com/sqlcipher/sqlcipher-android/issues/50 update(table, SQLiteDatabase.CONFLICT_NONE, values, whereClause, whereArgs ?: emptyArray()) fun SupportSQLiteDatabase.insert(table: String, values: ContentValues): Long = insert(table, SQLiteDatabase.CONFLICT_NONE, values) +fun SupportSQLiteDatabase.delete(table: String, whereClause: String?, whereArgs: Array?) = + delete(table, whereClause, whereArgs ?: emptyArray()) + /** * insert where conflicts are ignored instead of raising exception */ diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionDatabase.java b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionDatabase.java index 1a26b984ac..e8fc188c0c 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionDatabase.java +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionDatabase.java @@ -442,7 +442,7 @@ public void onOpen(@NonNull SupportSQLiteDatabase db) { "%1$s IN %2$s OR %3$s OR %4$s IN (SELECT %5$s FROM %6$s WHERE %3$s)", KEY_ROWID, uncommitedSelect, uncommitedParentSelect, KEY_TRANSFER_PEER, KEY_ROWID, TABLE_TRANSACTIONS); Timber.d(whereClause); - MoreDbUtilsKt.safeUpdateWithSealed(db, () -> db.delete(TABLE_TRANSACTIONS, whereClause, null)); + MoreDbUtilsKt.safeUpdateWithSealed(db, () -> MoreDbUtilsKt.delete(db, TABLE_TRANSACTIONS, whereClause, null)); } catch (SQLiteException e) { CrashHandler.report(e); } diff --git a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionProvider.java b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionProvider.java index 2eedf16460..366f136276 100644 --- a/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionProvider.java +++ b/myExpenses/src/main/java/org/totschnig/myexpenses/provider/TransactionProvider.java @@ -1110,7 +1110,7 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) { int uriMatch = URI_MATCHER.match(uri); maybeSetDirty(uriMatch); switch (uriMatch) { - case TRANSACTIONS, UNCOMMITTED -> count = db.delete(TABLE_TRANSACTIONS, where, whereArgs); + case TRANSACTIONS, UNCOMMITTED -> count = MoreDbUtilsKt.delete(db, TABLE_TRANSACTIONS, where, whereArgs); case TRANSACTION_ID -> { //maybe TODO ?: where and whereArgs are ignored segment = uri.getPathSegments().get(1); @@ -1129,8 +1129,8 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) { //we delete the transaction, its children and its transfer peer, and transfer peers of its children if (uri.getQueryParameter(QUERY_PARAMETER_MARK_VOID) == null) { //we delete the parent separately, so that the changes trigger can correctly record the parent uuid - count = db.delete(TABLE_TRANSACTIONS, WHERE_DEPENDENT, new String[]{segment, segment}); - count += db.delete(TABLE_TRANSACTIONS, WHERE_SELF_OR_PEER, new String[]{segment, segment}); + count = MoreDbUtilsKt.delete(db, TABLE_TRANSACTIONS, WHERE_DEPENDENT, new String[]{segment, segment}); + count += MoreDbUtilsKt.delete(db, TABLE_TRANSACTIONS, WHERE_SELF_OR_PEER, new String[]{segment, segment}); } else { ContentValues v = new ContentValues(); v.put(KEY_CR_STATUS, CrStatus.VOID.name()); @@ -1141,53 +1141,53 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) { db.endTransaction(); } } - case TEMPLATES -> count = db.delete(TABLE_TEMPLATES, where, whereArgs); - case TEMPLATE_ID -> count = db.delete(TABLE_TEMPLATES, + case TEMPLATES -> count = MoreDbUtilsKt.delete(db, TABLE_TEMPLATES, where, whereArgs); + case TEMPLATE_ID -> count = MoreDbUtilsKt.delete(db, TABLE_TEMPLATES, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); - case ACCOUNTTYPES_METHODS -> count = db.delete(TABLE_ACCOUNTTYES_METHODS, where, whereArgs); - case ACCOUNTS -> count = db.delete(TABLE_ACCOUNTS, where, whereArgs); - case ACCOUNT_ID -> count = db.delete(TABLE_ACCOUNTS, + case ACCOUNTTYPES_METHODS -> count = MoreDbUtilsKt.delete(db, TABLE_ACCOUNTTYES_METHODS, where, whereArgs); + case ACCOUNTS -> count = MoreDbUtilsKt.delete(db, TABLE_ACCOUNTS, where, whereArgs); + case ACCOUNT_ID -> count = MoreDbUtilsKt.delete(db, TABLE_ACCOUNTS, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); //update aggregate cursor //getContext().getContentResolver().notifyChange(AGGREGATES_URI, null); case CATEGORIES -> - count = db.delete(TABLE_CATEGORIES, KEY_ROWID + " != " + SPLIT_CATID + prefixAnd(where), + count = MoreDbUtilsKt.delete(db, TABLE_CATEGORIES, KEY_ROWID + " != " + SPLIT_CATID + prefixAnd(where), whereArgs); case CATEGORY_ID -> { String lastPathSegment = uri.getLastPathSegment(); if (Long.parseLong(lastPathSegment) == SPLIT_CATID) throw new IllegalArgumentException("split category can not be deleted"); - count = db.delete(TABLE_CATEGORIES, + count = MoreDbUtilsKt.delete(db, TABLE_CATEGORIES, KEY_ROWID + " = " + lastPathSegment + prefixAnd(where), whereArgs); } - case PAYEE_ID -> count = db.delete(TABLE_PAYEES, + case PAYEE_ID -> count = MoreDbUtilsKt.delete(db, TABLE_PAYEES, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); - case METHOD_ID -> count = db.delete(TABLE_METHODS, + case METHOD_ID -> count = MoreDbUtilsKt.delete(db, TABLE_METHODS, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); case PLANINSTANCE_TRANSACTION_STATUS -> - count = db.delete(TABLE_PLAN_INSTANCE_STATUS, where, whereArgs); + count = MoreDbUtilsKt.delete(db, TABLE_PLAN_INSTANCE_STATUS, where, whereArgs); case PLANINSTANCE_STATUS_SINGLE -> { - count = db.delete(TABLE_PLAN_INSTANCE_STATUS, + count = MoreDbUtilsKt.delete(db, TABLE_PLAN_INSTANCE_STATUS, String.format(Locale.ROOT, "%s = ? AND %s = ?", KEY_TEMPLATEID, KEY_INSTANCEID), new String[]{uri.getPathSegments().get(1), uri.getPathSegments().get(2)}); notifyChange(uri, false); return count; } - case EVENT_CACHE -> count = db.delete(TABLE_EVENT_CACHE, where, whereArgs); + case EVENT_CACHE -> count = MoreDbUtilsKt.delete(db, TABLE_EVENT_CACHE, where, whereArgs); case STALE_IMAGES_ID -> { //will fail if attachment is not stale segment = uri.getPathSegments().get(1); - count = db.delete(TABLE_ATTACHMENTS, KEY_ROWID + " = ?", new String[] { segment}); + count = MoreDbUtilsKt.delete(db, TABLE_ATTACHMENTS, KEY_ROWID + " = ?", new String[] { segment}); } - case STALE_IMAGES -> count = db.delete(TABLE_ATTACHMENTS, Companion.getSTALE_ATTACHMENT_SELECTION(), null); + case STALE_IMAGES -> count = MoreDbUtilsKt.delete(db, TABLE_ATTACHMENTS, Companion.getSTALE_ATTACHMENT_SELECTION(), null); - case CHANGES -> count = db.delete(TABLE_CHANGES, where, whereArgs); - case SETTINGS -> count = db.delete(TABLE_SETTINGS, where, whereArgs); + case CHANGES -> count = MoreDbUtilsKt.delete(db, TABLE_CHANGES, where, whereArgs); + case SETTINGS -> count = MoreDbUtilsKt.delete(db, TABLE_SETTINGS, where, whereArgs); case BUDGET_ID -> - count = db.delete(TABLE_BUDGETS, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); - case PAYEES -> count = db.delete(TABLE_PAYEES, where, whereArgs); - case TAG_ID -> count = db.delete(TABLE_TAGS, + count = MoreDbUtilsKt.delete(db, TABLE_BUDGETS, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); + case PAYEES -> count = MoreDbUtilsKt.delete(db, TABLE_PAYEES, where, whereArgs); + case TAG_ID -> count = MoreDbUtilsKt.delete(db, TABLE_TAGS, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); case DUAL -> { if ("1".equals(uri.getQueryParameter(QUERY_PARAMETER_SYNC_END))) { @@ -1202,7 +1202,7 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) { throw new IllegalArgumentException("Can only delete custom currencies"); } try { - count = db.delete(TABLE_CURRENCIES, String.format("%s = '%s'%s", KEY_CODE, + count = MoreDbUtilsKt.delete(db, TABLE_CURRENCIES, String.format("%s = '%s'%s", KEY_CODE, currency, prefixAnd(where)), whereArgs); } catch (SQLiteConstraintException e) { return 0; @@ -1210,26 +1210,26 @@ public int delete(@NonNull Uri uri, String where, String[] whereArgs) { } case TRANSACTIONS_TAGS -> { if (callerIsNotSyncAdapter(uri)) throw new IllegalArgumentException("Can only be called from sync adapter"); - count = db.delete(TABLE_TRANSACTIONS_TAGS, where, whereArgs); + count = MoreDbUtilsKt.delete(db, TABLE_TRANSACTIONS_TAGS, where, whereArgs); } case TEMPLATES_TAGS -> { - count = db.delete(TABLE_TEMPLATES_TAGS, where, whereArgs); + count = MoreDbUtilsKt.delete(db, TABLE_TEMPLATES_TAGS, where, whereArgs); } case ACCOUNTS_TAGS -> { - count = db.delete(TABLE_ACCOUNTS_TAGS, where, whereArgs); + count = MoreDbUtilsKt.delete(db, TABLE_ACCOUNTS_TAGS, where, whereArgs); } case DEBT_ID -> { - count = db.delete(TABLE_DEBTS, + count = MoreDbUtilsKt.delete(db, TABLE_DEBTS, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); } case BANK_ID -> { - count = db.delete(TABLE_BANKS, + count = MoreDbUtilsKt.delete(db, TABLE_BANKS, KEY_ROWID + " = " + uri.getLastPathSegment() + prefixAnd(where), whereArgs); } case TRANSACTION_ID_ATTACHMENT_ID -> { String transactionId = uri.getPathSegments().get(2); String attachmentId = uri.getPathSegments().get(3); - count = db.delete(TABLE_TRANSACTION_ATTACHMENTS, + count = MoreDbUtilsKt.delete(db, TABLE_TRANSACTION_ATTACHMENTS, KEY_TRANSACTIONID + " = ? AND " + KEY_ATTACHMENT_ID + " = ?", new String[] { transactionId, attachmentId } ); @@ -1690,7 +1690,7 @@ public static ContentProviderOperation resumeChangeTrigger() { } static int resumeChangeTrigger(SupportSQLiteDatabase db) { - return db.delete(TABLE_SYNC_STATE, null, null); + return MoreDbUtilsKt.delete(db, TABLE_SYNC_STATE, null, null); } public static ContentProviderOperation pauseChangeTrigger() { diff --git a/sqlcrypt/src/main/java/org/totschnig/sqlcrypt/SQLiteOpenHelperFactory.kt b/sqlcrypt/src/main/java/org/totschnig/sqlcrypt/SQLiteOpenHelperFactory.kt index 5d6a89800e..b47e41178e 100644 --- a/sqlcrypt/src/main/java/org/totschnig/sqlcrypt/SQLiteOpenHelperFactory.kt +++ b/sqlcrypt/src/main/java/org/totschnig/sqlcrypt/SQLiteOpenHelperFactory.kt @@ -3,8 +3,8 @@ package org.totschnig.sqlcrypt import android.content.Context import android.os.Build import androidx.annotation.Keep -import net.sqlcipher.database.SQLiteDatabase -import net.sqlcipher.database.SupportFactory +import net.zetetic.database.sqlcipher.SQLiteDatabase +import net.zetetic.database.sqlcipher.SupportOpenHelperFactory import org.totschnig.myexpenses.di.SqlCryptProvider import org.totschnig.myexpenses.util.crypt.PassphraseRepository import java.io.File @@ -35,12 +35,16 @@ class SQLiteOpenHelperFactory : SqlCryptProvider { } .getPassphrase() - override fun provideEncryptedDatabase(context: Context) = SupportFactory(passPhrase(context)) + override fun provideEncryptedDatabase(context: Context): SupportOpenHelperFactory { + System.loadLibrary("sqlcipher") + return SupportOpenHelperFactory(passPhrase(context)) + } /** * https://commonsware.com/Room/pages/chap-sqlciphermgmt-001.html */ override fun decrypt(context: Context, encrypted: File, backupDb: File) { + System.loadLibrary("sqlcipher") val originalDb = SQLiteDatabase.openDatabase( encrypted.absolutePath, passPhrase(context), @@ -52,7 +56,6 @@ class SQLiteOpenHelperFactory : SqlCryptProvider { SQLiteDatabase.openOrCreateDatabase( backupDb.absolutePath, - "", null ).close() // create an empty database @@ -68,7 +71,6 @@ class SQLiteOpenHelperFactory : SqlCryptProvider { SQLiteDatabase.openOrCreateDatabase( backupDb.absolutePath, - "", null ).use { it.version = version @@ -79,14 +81,13 @@ class SQLiteOpenHelperFactory : SqlCryptProvider { * https://commonsware.com/Room/pages/chap-sqlciphermgmt-001.html */ override fun encrypt(context: Context, backupFile: File, currentDb: File) { - SQLiteDatabase.loadLibs(context) + System.loadLibrary("sqlcipher") if (currentDb.exists()) { if (!currentDb.delete()) throw IOException("File $currentDb exists and cannot be deleted.") } val version = SQLiteDatabase.openDatabase( backupFile.absolutePath, - "", null, SQLiteDatabase.OPEN_READWRITE ).use { @@ -96,6 +97,7 @@ class SQLiteOpenHelperFactory : SqlCryptProvider { SQLiteDatabase.openOrCreateDatabase( currentDb.absolutePath, passPhrase(context), + null, null ).use { db -> //language=text