Merge main into fix-reporting-pii-mainthread
This commit is contained in:
@@ -6,6 +6,7 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.libremail.data.local.entity.FolderUnreadCount
|
||||
import org.libremail.data.local.entity.MessageEntity
|
||||
@@ -36,11 +37,14 @@ interface MessageDao {
|
||||
* by [pagingUnifiedFolderSearchSummaries] (issue #214). Profiling (see
|
||||
* `docs/perf/issue-124-unified-inbox-paging.md`) showed the first page loads flat regardless of
|
||||
* total cache size on the existing indices, so no `(folder, …)` index / schema migration is added.
|
||||
* Breaks ties on the `id` primary key (issue #311): `timestampMillis` isn't unique — bulk mail
|
||||
* shares a second — so without a unique tiebreaker two rows tied at a LIMIT/OFFSET page boundary
|
||||
* could duplicate or skip across pages. `id` is in the projection, so the tiebreaker adds no index.
|
||||
*/
|
||||
@Query(
|
||||
"SELECT id, accountId, sender, senderEmail, subject, snippet, timestampMillis, " +
|
||||
"isRead, isStarred, folder, inInbox, bodyFetched FROM messages " +
|
||||
"WHERE folder = :folder AND inInbox = 1 ORDER BY timestampMillis DESC",
|
||||
"WHERE folder = :folder AND inInbox = 1 ORDER BY timestampMillis DESC, id",
|
||||
)
|
||||
fun pagingUnifiedFolderSummaries(folder: String): PagingSource<Int, MessageSummary>
|
||||
|
||||
@@ -56,7 +60,7 @@ interface MessageDao {
|
||||
@Query(
|
||||
"SELECT id, accountId, sender, senderEmail, subject, snippet, timestampMillis, " +
|
||||
"isRead, isStarred, folder, inInbox, bodyFetched FROM messages " +
|
||||
"WHERE accountId = :accountId AND folder = :folder AND inInbox = 1 ORDER BY timestampMillis DESC",
|
||||
"WHERE accountId = :accountId AND folder = :folder AND inInbox = 1 ORDER BY timestampMillis DESC, id",
|
||||
)
|
||||
fun pagingFolderSummaries(accountId: String, folder: String): PagingSource<Int, MessageSummary>
|
||||
|
||||
@@ -70,13 +74,14 @@ interface MessageDao {
|
||||
* server-search hits `MailRepository.searchServer` inserts — exactly what the old filter saw.
|
||||
* Matches the Unicode-casefolded `*Fold` columns (issue #232) with a pattern built from the
|
||||
* lowercased query, so search is case-insensitive beyond ASCII — unlike the old ASCII-only `LIKE`.
|
||||
* Breaks ties on the `id` primary key for a total page order, like the browse pagers (issue #311).
|
||||
*/
|
||||
@Query(
|
||||
"SELECT id, accountId, sender, senderEmail, subject, snippet, timestampMillis, " +
|
||||
"isRead, isStarred, folder, inInbox, bodyFetched FROM messages " +
|
||||
"WHERE folder = :folder AND (senderFold LIKE :pattern ESCAPE '\\' OR " +
|
||||
"senderEmailFold LIKE :pattern ESCAPE '\\' OR subjectFold LIKE :pattern ESCAPE '\\' OR " +
|
||||
"snippetFold LIKE :pattern ESCAPE '\\') ORDER BY timestampMillis DESC",
|
||||
"snippetFold LIKE :pattern ESCAPE '\\') ORDER BY timestampMillis DESC, id",
|
||||
)
|
||||
fun pagingUnifiedFolderSearchSummaries(folder: String, pattern: String): PagingSource<Int, MessageSummary>
|
||||
|
||||
@@ -91,7 +96,7 @@ interface MessageDao {
|
||||
"isRead, isStarred, folder, inInbox, bodyFetched FROM messages " +
|
||||
"WHERE accountId = :accountId AND folder = :folder AND (senderFold LIKE :pattern ESCAPE '\\' OR " +
|
||||
"senderEmailFold LIKE :pattern ESCAPE '\\' OR subjectFold LIKE :pattern ESCAPE '\\' OR " +
|
||||
"snippetFold LIKE :pattern ESCAPE '\\') ORDER BY timestampMillis DESC",
|
||||
"snippetFold LIKE :pattern ESCAPE '\\') ORDER BY timestampMillis DESC, id",
|
||||
)
|
||||
fun pagingFolderSearchSummaries(
|
||||
accountId: String,
|
||||
@@ -190,6 +195,28 @@ interface MessageDao {
|
||||
uid: Long,
|
||||
)
|
||||
|
||||
/**
|
||||
* Applies [updateHeaderContent] to every [messages] row in a single transaction (issue #310). A
|
||||
* foreground sync / pull-to-refresh refreshes a whole recent window at once; running each row's
|
||||
* UPDATE in its own implicit transaction fsyncs the journal once per message — N commits per folder
|
||||
* per sync, amplified on the encrypted cache — so this collapses them into one commit. Refreshes the
|
||||
* same display fields (plus the materialized [MessageEntity.uid] and the casefold search columns) as
|
||||
* the single-row overload, still leaving cached bodies and optimistic read/star flags untouched.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun updateHeaderContents(messages: List<MessageEntity>) {
|
||||
for (message in messages) {
|
||||
updateHeaderContent(
|
||||
id = message.id,
|
||||
sender = message.sender,
|
||||
senderEmail = message.senderEmail,
|
||||
subject = message.subject,
|
||||
timestampMillis = message.timestampMillis,
|
||||
uid = message.uid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Marks rows as folder-synced (e.g. a former search-only row that the sync now returns). */
|
||||
@Query("UPDATE messages SET inInbox = 1 WHERE id IN (:ids)")
|
||||
suspend fun markSynced(ids: List<String>)
|
||||
|
||||
@@ -122,18 +122,11 @@ class MailSyncer @Inject constructor(
|
||||
val ids = entities.map { it.id }
|
||||
messageDao.insertNew(entities)
|
||||
// Mark every fetched message as synced (upgrades any former search-only row) and refresh
|
||||
// its display fields — without touching cached bodies or optimistic read/star flags.
|
||||
// its display fields — without touching cached bodies or optimistic read/star flags. The
|
||||
// per-row refreshes run in a single transaction (issue #310) so a whole recent window
|
||||
// costs one commit instead of one fsync per message (amplified on the encrypted cache).
|
||||
messageDao.markSynced(ids)
|
||||
entities.forEach {
|
||||
messageDao.updateHeaderContent(
|
||||
id = it.id,
|
||||
sender = it.sender,
|
||||
senderEmail = it.senderEmail,
|
||||
subject = it.subject,
|
||||
timestampMillis = it.timestampMillis,
|
||||
uid = it.uid,
|
||||
)
|
||||
}
|
||||
messageDao.updateHeaderContents(entities)
|
||||
// Reconcile server-side deletions ONLY within the fetched recent-UID window, so older
|
||||
// history paged in by the background backfill (issue #12) survives each foreground sync
|
||||
// instead of being wiped by a whole-folder "not in the recent 50" delete. Bound the
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.libremail.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -46,30 +47,39 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
/**
|
||||
* Every migration the Room builder registers, in one named list so [provideDatabase] and the
|
||||
* "registered == declared" safety-net test (issue #312) read the same source. There is deliberately
|
||||
* no destructive fallback (see below), so a migration authored + schema-committed but forgotten here
|
||||
* passes every replay test yet crash-loops ALL upgrading users at first DB open; `MigrationTest`
|
||||
* asserts this list equals the reflectively-discovered set of every `Migration` val in Migrations.kt.
|
||||
*/
|
||||
val ALL_MIGRATIONS: Array<Migration> = arrayOf(
|
||||
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,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
.addMigrations(*ALL_MIGRATIONS)
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user