Restores Unicode-aware case-insensitive substring search (approach A of #227). Paging (#223) moved search to a SQL LIKE scan whose case-folding is ASCII-only, so non-ASCII terms stopped matching case-insensitively. Adds per-field casefold columns to `messages` (senderFold/senderEmailFold/ subjectFold/snippetFold), each = Kotlin lowercase() of its source (Unicode-aware). Per-field (not one concatenated column) because the fields are maintained by partial UPDATEs that don't carry all four: toEntity sets all folds, updateBody keeps snippetFold in sync, updateHeaderContent keeps the header folds -- via thin DAO default-method wrappers so the five call sites are unchanged. Search matches the fold columns with a pattern built from the lowercased query. Schema v18->v19 (additive; ASCII lower() backfill, non-ASCII rows re-fold on next write). Adds a MigrationTest v18->v19 case and a repo test asserting the query is casefolded. Closes #232 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
4.9 KiB
Kotlin
117 lines
4.9 KiB
Kotlin
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package org.libremail.di
|
|
|
|
import android.content.Context
|
|
import androidx.room.Room
|
|
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
|
import dagger.Module
|
|
import dagger.Provides
|
|
import dagger.hilt.InstallIn
|
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
import dagger.hilt.components.SingletonComponent
|
|
import kotlinx.coroutines.runBlocking
|
|
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
|
|
import org.libremail.data.local.CacheOpenMode
|
|
import org.libremail.data.local.DatabaseFiles
|
|
import org.libremail.data.local.DatabaseProvisioner
|
|
import org.libremail.data.local.DeferredOpenHelperFactory
|
|
import org.libremail.data.local.LibreMailDatabase
|
|
import org.libremail.data.local.MIGRATION_10_11
|
|
import org.libremail.data.local.MIGRATION_11_12
|
|
import org.libremail.data.local.MIGRATION_12_13
|
|
import org.libremail.data.local.MIGRATION_13_14
|
|
import org.libremail.data.local.MIGRATION_14_15
|
|
import org.libremail.data.local.MIGRATION_15_16
|
|
import org.libremail.data.local.MIGRATION_16_17
|
|
import org.libremail.data.local.MIGRATION_17_18
|
|
import org.libremail.data.local.MIGRATION_18_19
|
|
import org.libremail.data.local.MIGRATION_1_2
|
|
import org.libremail.data.local.MIGRATION_2_3
|
|
import org.libremail.data.local.MIGRATION_3_4
|
|
import org.libremail.data.local.MIGRATION_4_5
|
|
import org.libremail.data.local.MIGRATION_5_6
|
|
import org.libremail.data.local.MIGRATION_6_7
|
|
import org.libremail.data.local.MIGRATION_7_8
|
|
import org.libremail.data.local.MIGRATION_8_9
|
|
import org.libremail.data.local.MIGRATION_9_10
|
|
import org.libremail.data.local.dao.AttachmentDao
|
|
import org.libremail.data.local.dao.BackfillProgressDao
|
|
import org.libremail.data.local.dao.DraftDao
|
|
import org.libremail.data.local.dao.FolderDao
|
|
import org.libremail.data.local.dao.MessageDao
|
|
import org.libremail.data.local.dao.OutboxDao
|
|
import javax.inject.Singleton
|
|
|
|
@Module
|
|
@InstallIn(SingletonComponent::class)
|
|
object DatabaseModule {
|
|
|
|
@Provides
|
|
@Singleton
|
|
fun provideDatabase(@ApplicationContext context: Context, provisioner: DatabaseProvisioner): LibreMailDatabase =
|
|
Room.databaseBuilder(context, LibreMailDatabase::class.java, DB_NAME)
|
|
.addMigrations(
|
|
MIGRATION_1_2,
|
|
MIGRATION_2_3,
|
|
MIGRATION_3_4,
|
|
MIGRATION_4_5,
|
|
MIGRATION_5_6,
|
|
MIGRATION_6_7,
|
|
MIGRATION_7_8,
|
|
MIGRATION_8_9,
|
|
MIGRATION_9_10,
|
|
MIGRATION_10_11,
|
|
MIGRATION_11_12,
|
|
MIGRATION_12_13,
|
|
MIGRATION_13_14,
|
|
MIGRATION_14_15,
|
|
MIGRATION_15_16,
|
|
MIGRATION_16_17,
|
|
MIGRATION_17_18,
|
|
MIGRATION_18_19,
|
|
)
|
|
// No destructive fallback: the migration chain is complete, and silently dropping the
|
|
// mail/message tables would lose cached data. A missing migration should fail loudly in
|
|
// testing instead.
|
|
//
|
|
// All blocking startup work — the issue-#111 AccountDataMigrator, the encrypted-cache
|
|
// conversion, and the Keystore passphrase resolution — is deferred OFF this injection path
|
|
// (issue #93). The factory below runs DatabaseProvisioner.prepareCache() lazily, when Room
|
|
// first OPENS the cache on its background query executor, never on the (possibly main)
|
|
// thread that injects this singleton. prepareCache() still performs that sequence before the
|
|
// file opens and in the same order, so the migrate-before-open guarantee and the encryption
|
|
// gate are unchanged — only where/when they run moved.
|
|
.openHelperFactory(
|
|
DeferredOpenHelperFactory { configuration ->
|
|
val realFactory = when (val mode = runBlocking { provisioner.prepareCache() }) {
|
|
is CacheOpenMode.Encrypted ->
|
|
SupportOpenHelperFactory(mode.passphrase.toByteArray(Charsets.US_ASCII), null, false)
|
|
|
|
CacheOpenMode.Plaintext -> FrameworkSQLiteOpenHelperFactory()
|
|
}
|
|
realFactory.create(configuration)
|
|
},
|
|
)
|
|
.build()
|
|
|
|
@Provides
|
|
fun provideMessageDao(database: LibreMailDatabase): MessageDao = database.messageDao()
|
|
|
|
@Provides
|
|
fun provideAttachmentDao(database: LibreMailDatabase): AttachmentDao = database.attachmentDao()
|
|
|
|
@Provides
|
|
fun provideOutboxDao(database: LibreMailDatabase): OutboxDao = database.outboxDao()
|
|
|
|
@Provides
|
|
fun provideDraftDao(database: LibreMailDatabase): DraftDao = database.draftDao()
|
|
|
|
@Provides
|
|
fun provideFolderDao(database: LibreMailDatabase): FolderDao = database.folderDao()
|
|
|
|
@Provides
|
|
fun provideBackfillProgressDao(database: LibreMailDatabase): BackfillProgressDao = database.backfillProgressDao()
|
|
|
|
private const val DB_NAME = DatabaseFiles.NAME
|
|
}
|