diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index db4a9c8c..1bbd6a14 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,12 +9,14 @@ plugins {
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.navigation.safeargs)
- alias(libs.plugins.androidx.room)
+ alias(libs.plugins.androidx.room3)
alias(libs.plugins.detekt.plugin)
// Rust Android Gradle plugin - COMMENTED OUT due to Gradle 9.2 incompatibility
// Use manual Rust build script instead (see rust-c2pa-ffi/build-android.sh)
// id("org.mozilla.rust-android-gradle.rust-android") version "0.9.4"
// Google Services plugins applied conditionally at bottom of file for GMS builds only
+
+ alias(libs.plugins.koin.compiler)
}
fun loadLocalProperties(): Properties = Properties().apply {
@@ -189,7 +191,7 @@ base {
archivesName.set("save-${project.version}")
}
-room {
+room3 {
schemaDirectory("$projectDir/schemas")
}
@@ -268,9 +270,8 @@ dependencies {
implementation(libs.androidx.work)
// Room Database
- implementation(libs.androidx.room.runtime)
- implementation(libs.androidx.room.ktx)
- ksp(libs.androidx.room.compiler)
+ implementation(libs.androidx.room3.runtime)
+ ksp(libs.androidx.room3.compiler)
// Dependency Injection - Koin
implementation(libs.koin.core)
@@ -281,6 +282,8 @@ dependencies {
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.compose.viewmodel.navigation)
+ implementation(libs.koin.annotations)
+
// Networking
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
diff --git a/app/schemas/net.opendasharchive.openarchive.db.AppDatabase/3.json b/app/schemas/net.opendasharchive.openarchive.db.AppDatabase/3.json
new file mode 100644
index 00000000..2e0a9b3d
--- /dev/null
+++ b/app/schemas/net.opendasharchive.openarchive.db.AppDatabase/3.json
@@ -0,0 +1,597 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "7c435845a0996d16ffad32c0f0af9dc7",
+ "entities": [
+ {
+ "tableName": "vaults",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `host` TEXT NOT NULL, `metaData` TEXT NOT NULL, `licenseUrl` TEXT, `createdAt` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "host",
+ "columnName": "host",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "metaData",
+ "columnName": "metaData",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "licenseUrl",
+ "columnName": "licenseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "archives",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `description` TEXT, `createdAt` INTEGER, `vaultId` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `openSubmissionId` INTEGER NOT NULL, `licenseUrl` TEXT, `isRemote` INTEGER NOT NULL, FOREIGN KEY(`vaultId`) REFERENCES `vaults`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "vaultId",
+ "columnName": "vaultId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "archived",
+ "columnName": "archived",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "openSubmissionId",
+ "columnName": "openSubmissionId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "licenseUrl",
+ "columnName": "licenseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isRemote",
+ "columnName": "isRemote",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_archives_vaultId",
+ "unique": false,
+ "columnNames": [
+ "vaultId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_archives_vaultId` ON `${TABLE_NAME}` (`vaultId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "vaults",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "vaultId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "submissions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `archiveId` INTEGER NOT NULL, `uploadedAt` INTEGER, `serverUrl` TEXT, FOREIGN KEY(`archiveId`) REFERENCES `archives`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "archiveId",
+ "columnName": "archiveId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uploadedAt",
+ "columnName": "uploadedAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "serverUrl",
+ "columnName": "serverUrl",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_submissions_archiveId",
+ "unique": false,
+ "columnNames": [
+ "archiveId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_submissions_archiveId` ON `${TABLE_NAME}` (`archiveId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "archives",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "archiveId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "evidence",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `originalFilePath` TEXT NOT NULL, `mimeType` TEXT NOT NULL, `createdAt` INTEGER, `updatedAt` INTEGER, `uploadedAt` INTEGER, `serverUrl` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `author` TEXT NOT NULL, `location` TEXT NOT NULL, `tags` TEXT NOT NULL, `licenseUrl` TEXT, `mediaHashString` TEXT NOT NULL, `status` INTEGER NOT NULL, `statusMessage` TEXT NOT NULL, `archiveId` INTEGER NOT NULL, `submissionId` INTEGER NOT NULL, `contentLength` INTEGER NOT NULL, `progress` INTEGER NOT NULL, `flag` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `thumbnail` BLOB, FOREIGN KEY(`archiveId`) REFERENCES `archives`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`submissionId`) REFERENCES `submissions`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "originalFilePath",
+ "columnName": "originalFilePath",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "uploadedAt",
+ "columnName": "uploadedAt",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "serverUrl",
+ "columnName": "serverUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "licenseUrl",
+ "columnName": "licenseUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "mediaHashString",
+ "columnName": "mediaHashString",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusMessage",
+ "columnName": "statusMessage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "archiveId",
+ "columnName": "archiveId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "submissionId",
+ "columnName": "submissionId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentLength",
+ "columnName": "contentLength",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "progress",
+ "columnName": "progress",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flag",
+ "columnName": "flag",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "thumbnail",
+ "columnName": "thumbnail",
+ "affinity": "BLOB"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_evidence_archiveId",
+ "unique": false,
+ "columnNames": [
+ "archiveId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_evidence_archiveId` ON `${TABLE_NAME}` (`archiveId`)"
+ },
+ {
+ "name": "index_evidence_submissionId",
+ "unique": false,
+ "columnNames": [
+ "submissionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_evidence_submissionId` ON `${TABLE_NAME}` (`submissionId`)"
+ },
+ {
+ "name": "index_evidence_status",
+ "unique": false,
+ "columnNames": [
+ "status"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_evidence_status` ON `${TABLE_NAME}` (`status`)"
+ },
+ {
+ "name": "index_evidence_priority",
+ "unique": false,
+ "columnNames": [
+ "priority"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_evidence_priority` ON `${TABLE_NAME}` (`priority`)"
+ },
+ {
+ "name": "index_evidence_createdAt",
+ "unique": false,
+ "columnNames": [
+ "createdAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_evidence_createdAt` ON `${TABLE_NAME}` (`createdAt`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "archives",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "archiveId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "submissions",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "submissionId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "migration_state",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `stage` TEXT NOT NULL, `processedCount` INTEGER NOT NULL, `totalCount` INTEGER NOT NULL, `completedAt` INTEGER, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "stage",
+ "columnName": "stage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "processedCount",
+ "columnName": "processedCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "totalCount",
+ "columnName": "totalCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "completedAt",
+ "columnName": "completedAt",
+ "affinity": "INTEGER"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "vault_dweb_metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`vaultId` INTEGER NOT NULL, `vaultKey` TEXT NOT NULL, PRIMARY KEY(`vaultId`), FOREIGN KEY(`vaultId`) REFERENCES `vaults`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "vaultId",
+ "columnName": "vaultId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "vaultKey",
+ "columnName": "vaultKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "vaultId"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "vaults",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "vaultId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "archive_dweb_metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`archiveId` INTEGER NOT NULL, `archiveKey` TEXT NOT NULL, `archiveHash` TEXT NOT NULL, `permissions` TEXT NOT NULL, PRIMARY KEY(`archiveId`), FOREIGN KEY(`archiveId`) REFERENCES `archives`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "archiveId",
+ "columnName": "archiveId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "archiveKey",
+ "columnName": "archiveKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "archiveHash",
+ "columnName": "archiveHash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "archiveId"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "archives",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "archiveId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "evidence_dweb_metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`evidenceId` INTEGER NOT NULL, `isDownloaded` INTEGER NOT NULL, PRIMARY KEY(`evidenceId`), FOREIGN KEY(`evidenceId`) REFERENCES `evidence`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "evidenceId",
+ "columnName": "evidenceId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDownloaded",
+ "columnName": "isDownloaded",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "evidenceId"
+ ]
+ },
+ "foreignKeys": [
+ {
+ "table": "evidence",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "evidenceId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7c435845a0996d16ffad32c0f0af9dc7')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/DatabaseModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/DatabaseModule.kt
index 95569798..fa58740b 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/di/DatabaseModule.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/DatabaseModule.kt
@@ -1,6 +1,6 @@
package net.opendasharchive.openarchive.core.di
-import androidx.room.Room
+import androidx.room3.Room
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.Dispatchers
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/domain/Evidence.kt b/app/src/main/java/net/opendasharchive/openarchive/core/domain/Evidence.kt
index b4fd6dad..83120696 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/domain/Evidence.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/domain/Evidence.kt
@@ -15,6 +15,7 @@ import java.io.File
data class Evidence(
val id: Long = 0L,
val originalFilePath: String = "",
+ val thumbnail: ByteArray? = null,
val mimeType: String = "",
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null,
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/Mappers.kt b/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/Mappers.kt
index 5d69a81f..0ea87305 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/Mappers.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/Mappers.kt
@@ -106,6 +106,7 @@ fun Submission.toSubmissionEntity(): SubmissionEntity = SubmissionEntity(
fun EvidenceEntity.toDomain(vaultId: Long = 0L): Evidence = Evidence(
id = this.id,
originalFilePath = this.originalFilePath,
+ thumbnail = this.thumbnail,
mimeType = this.mimeType,
createdAt = this.createdAt,
updatedAt = this.updatedAt,
@@ -157,7 +158,8 @@ fun Evidence.toEvidenceEntity(): EvidenceEntity = EvidenceEntity(
contentLength = this.contentLength,
progress = this.progress,
flag = this.isFlagged,
- priority = this.priority
+ priority = this.priority,
+ thumbnail = this.thumbnail
)
fun Evidence.toDwebEntity(): EvidenceDwebEntity = EvidenceDwebEntity(
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/SugarMappers.kt b/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/SugarMappers.kt
index 97ef8902..b9d616a2 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/SugarMappers.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/domain/mappers/SugarMappers.kt
@@ -104,6 +104,7 @@ fun Submission.toEntity(): SugarCollection {
fun Media.toDomain(): Evidence = Evidence(
id = this.id ?: 0L,
originalFilePath = this.originalFilePath,
+ thumbnail = null,
mimeType = this.mimeType,
createdAt = this.createDate?.toKotlinLocalDateTime(),
updatedAt = this.updateDate?.toKotlinLocalDateTime(),
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/media/MediaThumbnail.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/media/MediaThumbnail.kt
index b60f7779..32ced48e 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/media/MediaThumbnail.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/media/MediaThumbnail.kt
@@ -50,6 +50,9 @@ fun MediaThumbnail(
onTitleVisibilityChanged: ((Boolean) -> Unit)? = null
) {
val context = LocalContext.current
+ val storedThumbnail = remember(evidence.thumbnail) {
+ evidence.thumbnail?.takeIf { it.isNotEmpty() }
+ }
val imageExists = remember(evidence.originalFilePath) {
runCatching { evidence.file.exists() }.getOrDefault(false)
}
@@ -98,6 +101,31 @@ fun MediaThumbnail(
onTitleVisibilityChanged?.invoke(false)
}
+ storedThumbnail != null -> {
+ SubcomposeAsyncImage(
+ model = ImageRequest.Builder(context)
+ .data(storedThumbnail)
+ .error(
+ when {
+ evidence.mimeType.startsWith("video") -> R.drawable.ic_video
+ evidence.mimeType == "application/pdf" -> R.drawable.ic_pdf
+ evidence.mimeType.startsWith("audio") -> R.drawable.ic_music
+ evidence.mimeType.startsWith("image") -> R.drawable.ic_image
+ else -> R.drawable.ic_unknown_file
+ }
+ )
+ .build(),
+ contentDescription = null,
+ contentScale = contentScale,
+ modifier = Modifier
+ .fillMaxSize()
+ .alpha(alpha)
+ ) {
+ SubcomposeAsyncImageContent()
+ }
+ onTitleVisibilityChanged?.invoke(false)
+ }
+
evidence.mimeType.startsWith("video") -> {
MediaPlaceholderIcon(
drawableRes = R.drawable.ic_video,
diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt b/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt
index e91f504a..833d61ae 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt
@@ -12,51 +12,55 @@ import java.io.File
*/
class FileCleanupHelper(private val context: Context) {
+ /**
+ * Deletes local upload artifacts after a successful upload while keeping the DB record.
+ */
+ fun deleteUploadedMediaFiles(evidence: Evidence) {
+ deleteInternalMediaFile(evidence)
+ deleteC2paSidecar(evidence.mediaHashString)
+ }
+
/**
* Deletes physical files associated with an Evidence domain object.
*/
fun deleteMediaFiles(evidence: Evidence) {
- // Delete original media file
- if (evidence.originalFilePath.isNotEmpty()) {
- try {
- val file = evidence.file
- if (isInternalFile(file)) {
- if (file.exists() && file.delete()) {
- AppLogger.i("Deleted internal media file: ${file.path}")
- }
- }
- } catch (e: Exception) {
- AppLogger.e("Failed to delete media file for ${evidence.id}", e)
- }
- }
-
- // Delete C2PA sidecar manifest
- if (evidence.mediaHashString.isNotEmpty()) {
- C2paHelper.removeC2paFiles(context, evidence.mediaHashString)
- }
+ deleteUploadedMediaFiles(evidence)
}
/**
* Deletes physical files associated with a legacy Media Sugar entity.
*/
fun deleteMediaFiles(media: Media) {
- // Delete original media file
if (media.originalFilePath.isNotEmpty()) {
try {
val file = media.file
- if (isInternalFile(file)) {
- if (file.exists() && file.delete()) {
- AppLogger.i("Deleted internal media file: ${file.path}")
- }
+ if (isInternalFile(file) && file.exists() && file.delete()) {
+ AppLogger.i("Deleted internal media file: ${file.path}")
}
} catch (e: Exception) {
AppLogger.e("Failed to delete legacy media file for ${media.id}", e)
}
}
- // Delete C2PA sidecar manifest
- if (media.mediaHashString.isNotEmpty()) {
- C2paHelper.removeC2paFiles(context, media.mediaHashString)
+ deleteC2paSidecar(media.mediaHashString)
+ }
+
+ private fun deleteInternalMediaFile(evidence: Evidence) {
+ if (evidence.originalFilePath.isNotEmpty()) {
+ try {
+ val file = evidence.file
+ if (isInternalFile(file) && file.exists() && file.delete()) {
+ AppLogger.i("Deleted internal media file: ${file.path}")
+ }
+ } catch (e: Exception) {
+ AppLogger.e("Failed to delete media file for ${evidence.id}", e)
+ }
+ }
+ }
+
+ private fun deleteC2paSidecar(mediaHashString: String) {
+ if (mediaHashString.isNotEmpty()) {
+ C2paHelper.removeC2paFiles(context, mediaHashString)
}
}
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/AppDatabase.kt b/app/src/main/java/net/opendasharchive/openarchive/db/AppDatabase.kt
index b447729f..42ed582c 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/AppDatabase.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/AppDatabase.kt
@@ -1,12 +1,14 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Database
-import androidx.room.RoomDatabase
-import androidx.room.TypeConverters
-import androidx.room.AutoMigration
-import androidx.room.DeleteColumn
-import androidx.room.migration.AutoMigrationSpec
+import android.annotation.SuppressLint
+import androidx.room3.Database
+import androidx.room3.RoomDatabase
+import androidx.room3.TypeConverters
+import androidx.room3.AutoMigration
+import androidx.room3.DeleteColumn
+import androidx.room3.migration.AutoMigrationSpec
+@SuppressLint("RestrictedApi")
@Database(
entities = [
VaultEntity::class,
@@ -23,9 +25,13 @@ import androidx.room.migration.AutoMigrationSpec
from = 1,
to = 2,
spec = RemoveVaultPasswordColumnMigration::class
+ ),
+ AutoMigration(
+ from = 2,
+ to = 3
)
],
- version = 2,
+ version = 3,
exportSchema = true
)
@TypeConverters(Converters::class)
@@ -39,4 +45,4 @@ abstract class AppDatabase : RoomDatabase() {
}
@DeleteColumn(tableName = "vaults", columnName = "password")
-class RemoveVaultPasswordColumnMigration : AutoMigrationSpec
\ No newline at end of file
+class RemoveVaultPasswordColumnMigration : AutoMigrationSpec
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDao.kt b/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDao.kt
index 1ce01031..45431a02 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDao.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDao.kt
@@ -1,6 +1,6 @@
package net.opendasharchive.openarchive.db
-import androidx.room.*
+import androidx.room3.*
import kotlinx.coroutines.flow.Flow
@Dao
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDwebEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDwebEntity.kt
index f58906f6..b0a06672 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDwebEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveDwebEntity.kt
@@ -1,8 +1,8 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.ForeignKey
+import androidx.room3.PrimaryKey
import net.opendasharchive.openarchive.core.domain.ArchivePermission
@Entity(
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveEntity.kt
index aa7098a2..ac16f9cc 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/ArchiveEntity.kt
@@ -1,9 +1,9 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.Index
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.ForeignKey
+import androidx.room3.Index
+import androidx.room3.PrimaryKey
import kotlinx.datetime.LocalDateTime
@Entity(
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/Converters.kt b/app/src/main/java/net/opendasharchive/openarchive/db/Converters.kt
index 8c479b39..315d543f 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/Converters.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/Converters.kt
@@ -1,6 +1,6 @@
package net.opendasharchive.openarchive.db
-import androidx.room.TypeConverter
+import androidx.room3.TypeConverter
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/DwebCompositeEntities.kt b/app/src/main/java/net/opendasharchive/openarchive/db/DwebCompositeEntities.kt
index 20a4cb78..c427a3d3 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/DwebCompositeEntities.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/DwebCompositeEntities.kt
@@ -1,7 +1,7 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Embedded
-import androidx.room.Relation
+import androidx.room3.Embedded
+import androidx.room3.Relation
data class VaultWithDweb(
@Embedded val vault: VaultEntity,
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/DwebDao.kt b/app/src/main/java/net/opendasharchive/openarchive/db/DwebDao.kt
index ed07080d..b0392344 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/DwebDao.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/DwebDao.kt
@@ -1,6 +1,6 @@
package net.opendasharchive.openarchive.db
-import androidx.room.*
+import androidx.room3.*
import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.LocalDateTime
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt
index dfe2e47f..2b91d91e 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt
@@ -1,6 +1,6 @@
package net.opendasharchive.openarchive.db
-import androidx.room.*
+import androidx.room3.*
import kotlinx.coroutines.flow.Flow
import net.opendasharchive.openarchive.core.domain.EvidenceStatus
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDwebEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDwebEntity.kt
index babe4058..e24cc1a9 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDwebEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDwebEntity.kt
@@ -1,8 +1,8 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.ForeignKey
+import androidx.room3.PrimaryKey
@Entity(
tableName = "evidence_dweb_metadata",
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceEntity.kt
index b274cf69..e592a5c2 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceEntity.kt
@@ -1,9 +1,9 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.Index
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.ForeignKey
+import androidx.room3.Index
+import androidx.room3.PrimaryKey
import kotlinx.datetime.LocalDateTime
import net.opendasharchive.openarchive.core.domain.EvidenceStatus
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/MigrationDao.kt b/app/src/main/java/net/opendasharchive/openarchive/db/MigrationDao.kt
index 033fb5b1..275f1854 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/MigrationDao.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/MigrationDao.kt
@@ -1,6 +1,6 @@
package net.opendasharchive.openarchive.db
-import androidx.room.*
+import androidx.room3.*
@Dao
interface MigrationDao {
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/MigrationStateEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/MigrationStateEntity.kt
index 7bb1a0b8..57239c58 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/MigrationStateEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/MigrationStateEntity.kt
@@ -1,7 +1,7 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.PrimaryKey
@Entity(tableName = "migration_state")
data class MigrationStateEntity(
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionDao.kt b/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionDao.kt
index 3763426d..785658b6 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionDao.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionDao.kt
@@ -1,11 +1,11 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import androidx.room.Upsert
+import androidx.room3.Dao
+import androidx.room3.Delete
+import androidx.room3.Insert
+import androidx.room3.OnConflictStrategy
+import androidx.room3.Query
+import androidx.room3.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionEntity.kt
index dde29956..021463ad 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/SubmissionEntity.kt
@@ -1,9 +1,9 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.Index
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.ForeignKey
+import androidx.room3.Index
+import androidx.room3.PrimaryKey
import kotlinx.datetime.LocalDateTime
@Entity(
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/VaultDao.kt b/app/src/main/java/net/opendasharchive/openarchive/db/VaultDao.kt
index 99d0de88..cd019d91 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/VaultDao.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/VaultDao.kt
@@ -1,6 +1,6 @@
package net.opendasharchive.openarchive.db
-import androidx.room.*
+import androidx.room3.*
import kotlinx.coroutines.flow.Flow
@Dao
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/VaultDwebEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/VaultDwebEntity.kt
index 5be8196a..3ec729f7 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/VaultDwebEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/VaultDwebEntity.kt
@@ -1,8 +1,8 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.ForeignKey
+import androidx.room3.PrimaryKey
@Entity(
tableName = "vault_dweb_metadata",
diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/VaultEntity.kt b/app/src/main/java/net/opendasharchive/openarchive/db/VaultEntity.kt
index bdcfeca3..8b5d800a 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/db/VaultEntity.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/db/VaultEntity.kt
@@ -1,7 +1,7 @@
package net.opendasharchive.openarchive.db
-import androidx.room.Entity
-import androidx.room.PrimaryKey
+import androidx.room3.Entity
+import androidx.room3.PrimaryKey
import kotlinx.datetime.LocalDateTime
import net.opendasharchive.openarchive.core.domain.VaultType
diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt
index 161ad5e1..62d6b781 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt
@@ -8,6 +8,7 @@ import net.opendasharchive.openarchive.core.domain.Evidence
import net.opendasharchive.openarchive.core.logger.AppLogger
import net.opendasharchive.openarchive.util.C2paHelper
import net.opendasharchive.openarchive.util.DateUtils
+import net.opendasharchive.openarchive.util.MediaThumbnailGenerator
import net.opendasharchive.openarchive.util.Prefs
import net.opendasharchive.openarchive.util.Utility
import net.opendasharchive.openarchive.util.toLocalDateTime
@@ -106,12 +107,20 @@ object MediaPicker {
""
}
+ val thumbnail = try {
+ file?.let { MediaThumbnailGenerator.generateThumbnailBytes(it, mimeType) }
+ } catch (e: Exception) {
+ AppLogger.e("Failed to generate thumbnail for media", e)
+ null
+ }
+
// Create domain object
val evidence = Evidence(
archiveId = archive.id,
submissionId = submissionId,
title = title,
originalFilePath = originalFilePath,
+ thumbnail = thumbnail,
mimeType = mimeType,
contentLength = contentLength,
createdAt = createDate.toLocalDateTime(),
diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/data/SnowbirdMappers.kt b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/data/SnowbirdMappers.kt
index ebb45d04..5e614f79 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/data/SnowbirdMappers.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/services/snowbird/data/SnowbirdMappers.kt
@@ -124,6 +124,7 @@ fun EvidenceWithDweb.toDomain(): Evidence {
return Evidence(
id = evidence.id,
originalFilePath = evidence.originalFilePath,
+ thumbnail = evidence.thumbnail,
mimeType = evidence.mimeType,
createdAt = evidence.createdAt ?: DateUtils.nowDateTime,
updatedAt = evidence.updatedAt ?: DateUtils.nowDateTime,
diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt
index a5816330..9b6ccf79 100644
--- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt
+++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt
@@ -25,6 +25,7 @@ import net.opendasharchive.openarchive.core.domain.Evidence
import net.opendasharchive.openarchive.core.domain.EvidenceStatus
import net.opendasharchive.openarchive.core.logger.AppLogger
import net.opendasharchive.openarchive.core.repositories.CollectionRepository
+import net.opendasharchive.openarchive.core.repositories.FileCleanupHelper
import net.opendasharchive.openarchive.core.repositories.MediaRepository
import net.opendasharchive.openarchive.core.repositories.ProjectRepository
import net.opendasharchive.openarchive.core.repositories.SpaceRepository
@@ -44,6 +45,7 @@ class UploadService : JobService() {
private val projectRepository: ProjectRepository by inject()
private val collectionRepository: CollectionRepository by inject()
private val spaceRepository: SpaceRepository by inject()
+ private val fileCleanupHelper: FileCleanupHelper by inject()
companion object {
private const val NOTIFICATION_CHANNEL_ID = "oasave_channel_1"
@@ -184,6 +186,9 @@ class UploadService : JobService() {
AppLogger.i("Started uploading", updatedMedia)
val uploadSuccess = upload(updatedMedia)
if (uploadSuccess) {
+ serviceScope.launch {
+ fileCleanupHelper.deleteUploadedMediaFiles(updatedMedia)
+ }
successCount++
} else {
failedCount++
diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/MediaThumbnailGenerator.kt b/app/src/main/java/net/opendasharchive/openarchive/util/MediaThumbnailGenerator.kt
new file mode 100644
index 00000000..48c69dde
--- /dev/null
+++ b/app/src/main/java/net/opendasharchive/openarchive/util/MediaThumbnailGenerator.kt
@@ -0,0 +1,147 @@
+package net.opendasharchive.openarchive.util
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.pdf.PdfRenderer
+import android.media.MediaMetadataRetriever
+import android.os.ParcelFileDescriptor
+import net.opendasharchive.openarchive.core.logger.AppLogger
+import java.io.ByteArrayOutputStream
+import java.io.File
+
+object MediaThumbnailGenerator {
+ private const val MAX_DIMENSION_PX = 128
+ private const val JPEG_QUALITY = 40
+
+ fun generateThumbnailBytes(file: File, mimeType: String): ByteArray? {
+ if (!file.exists()) return null
+
+ val bitmap = try {
+ when {
+ mimeType.startsWith("image/") -> createImageThumbnail(file)
+ mimeType.startsWith("video/") -> createVideoThumbnail(file)
+ mimeType == "application/pdf" -> createPdfThumbnail(file)
+ else -> null
+ }
+ } catch (e: Exception) {
+ AppLogger.w("Failed to generate thumbnail for ${file.name}: ${e.message}")
+ null
+ } ?: return null
+
+ return bitmap.useCompressedJpeg()
+ }
+
+ private fun createImageThumbnail(file: File): Bitmap? {
+ val bounds = BitmapFactory.Options().apply {
+ inJustDecodeBounds = true
+ }
+ BitmapFactory.decodeFile(file.absolutePath, bounds)
+
+ if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
+
+ val sampled = BitmapFactory.decodeFile(
+ file.absolutePath,
+ BitmapFactory.Options().apply {
+ inSampleSize = calculateInSampleSize(bounds.outWidth, bounds.outHeight, MAX_DIMENSION_PX)
+ inPreferredConfig = Bitmap.Config.RGB_565
+ }
+ ) ?: return null
+
+ return sampled.scaleDown(MAX_DIMENSION_PX)
+ }
+
+ private fun createVideoThumbnail(file: File): Bitmap? {
+ val retriever = MediaMetadataRetriever()
+ return try {
+ retriever.setDataSource(file.absolutePath)
+ val frame = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
+ frame?.scaleDown(MAX_DIMENSION_PX)
+ } finally {
+ runCatching { retriever.release() }
+ }
+ }
+
+ private fun createPdfThumbnail(file: File): Bitmap? {
+ val descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+ return try {
+ PdfRenderer(descriptor).use { renderer ->
+ if (renderer.pageCount == 0) return null
+
+ renderer.openPage(0).use { page ->
+ val scale = minOf(
+ MAX_DIMENSION_PX.toFloat() / page.width,
+ MAX_DIMENSION_PX.toFloat() / page.height
+ ).coerceAtLeast(0.1f)
+
+ val bitmap = Bitmap.createBitmap(
+ (page.width * scale).toInt().coerceAtLeast(1),
+ (page.height * scale).toInt().coerceAtLeast(1),
+ Bitmap.Config.ARGB_8888
+ )
+
+ Canvas(bitmap).drawColor(Color.WHITE)
+ val matrix = Matrix().apply { postScale(scale, scale) }
+
+ page.render(
+ bitmap,
+ null,
+ matrix,
+ PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
+ )
+
+ bitmap
+ }
+ }
+ } finally {
+ descriptor.close()
+ }
+ }
+
+ private fun Bitmap.scaleDown(maxDimensionPx: Int): Bitmap {
+ if (width <= maxDimensionPx && height <= maxDimensionPx) return this
+
+ val scale = minOf(
+ maxDimensionPx.toFloat() / width,
+ maxDimensionPx.toFloat() / height
+ )
+ val scaled = Bitmap.createScaledBitmap(
+ this,
+ (width * scale).toInt().coerceAtLeast(1),
+ (height * scale).toInt().coerceAtLeast(1),
+ true
+ )
+
+ if (scaled != this) {
+ recycle()
+ }
+ return scaled
+ }
+
+ private fun Bitmap.useCompressedJpeg(): ByteArray? {
+ return try {
+ ByteArrayOutputStream().use { output ->
+ compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, output)
+ output.toByteArray()
+ }
+ } finally {
+ recycle()
+ }
+ }
+
+ private fun calculateInSampleSize(width: Int, height: Int, maxDimensionPx: Int): Int {
+ var inSampleSize = 1
+ var currentWidth = width
+ var currentHeight = height
+
+ while (currentWidth / 2 >= maxDimensionPx || currentHeight / 2 >= maxDimensionPx) {
+ inSampleSize *= 2
+ currentWidth /= 2
+ currentHeight /= 2
+ }
+
+ return inSampleSize.coerceAtLeast(1)
+ }
+}
diff --git a/app/src/test/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelperTest.kt b/app/src/test/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelperTest.kt
new file mode 100644
index 00000000..5ea46b32
--- /dev/null
+++ b/app/src/test/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelperTest.kt
@@ -0,0 +1,36 @@
+package net.opendasharchive.openarchive.core.repositories
+
+import android.app.Application
+import android.net.Uri
+import net.opendasharchive.openarchive.core.domain.Evidence
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import java.io.File
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
+class FileCleanupHelperTest {
+
+ @Test
+ fun `deleteUploadedMediaFiles removes internal file and keeps db-backed thumbnail untouched`() {
+ val context = RuntimeEnvironment.getApplication()
+ val file = File(context.filesDir, "cleanup-uploaded-file.jpg").apply {
+ writeBytes(byteArrayOf(1, 2, 3))
+ }
+ val evidence = Evidence(
+ originalFilePath = Uri.fromFile(file).toString(),
+ thumbnail = byteArrayOf(9, 8, 7),
+ mediaHashString = ""
+ )
+
+ FileCleanupHelper(context).deleteUploadedMediaFiles(evidence)
+
+ assertFalse(file.exists())
+ assertTrue(evidence.thumbnail!!.isNotEmpty())
+ }
+}
diff --git a/app/src/test/java/net/opendasharchive/openarchive/util/MediaThumbnailGeneratorTest.kt b/app/src/test/java/net/opendasharchive/openarchive/util/MediaThumbnailGeneratorTest.kt
new file mode 100644
index 00000000..fad1c75c
--- /dev/null
+++ b/app/src/test/java/net/opendasharchive/openarchive/util/MediaThumbnailGeneratorTest.kt
@@ -0,0 +1,44 @@
+package net.opendasharchive.openarchive.util
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import java.io.File
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = android.app.Application::class)
+class MediaThumbnailGeneratorTest {
+
+ @Test
+ fun `generateThumbnailBytes creates a small jpeg thumbnail for image files`() {
+ val source = File.createTempFile("thumbnail-source", ".jpg")
+ source.outputStream().use { output ->
+ Bitmap.createBitmap(1200, 800, Bitmap.Config.ARGB_8888).apply {
+ eraseColor(android.graphics.Color.RED)
+ compress(Bitmap.CompressFormat.JPEG, 95, output)
+ recycle()
+ }
+ }
+
+ val thumbnailBytes = MediaThumbnailGenerator.generateThumbnailBytes(
+ file = source,
+ mimeType = "image/jpeg"
+ )
+
+ assertNotNull(thumbnailBytes)
+ assertTrue(thumbnailBytes!!.size in 1 until 20_000)
+
+ val bitmap = BitmapFactory.decodeByteArray(thumbnailBytes, 0, thumbnailBytes.size)
+ assertNotNull(bitmap)
+ assertTrue(bitmap!!.width <= 128)
+ assertTrue(bitmap.height <= 128)
+
+ bitmap.recycle()
+ source.delete()
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 76bef054..aec05d1e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,7 +10,7 @@ plugins {
// Build Tools
alias(libs.plugins.ksp) apply false
alias(libs.plugins.navigation.safeargs) apply false
- alias(libs.plugins.androidx.room) apply false
+ alias(libs.plugins.androidx.room3) apply false
// Code Quality
alias(libs.plugins.detekt.plugin) apply false
diff --git a/config/baseline.xml b/config/baseline.xml
index d39e5777..f2f0eada 100644
--- a/config/baseline.xml
+++ b/config/baseline.xml
@@ -862,9 +862,9 @@
ImportOrdering:CameraScreen.kt$import android.content.Context import android.net.Uri import android.util.Size import androidx.camera.compose.CameraXViewfinder import androidx.camera.core.Camera import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import androidx.camera.core.Preview import androidx.camera.core.SurfaceRequest import androidx.camera.core.resolutionselector.ResolutionSelector import androidx.camera.core.resolutionselector.ResolutionStrategy import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.video.Quality import androidx.camera.video.QualitySelector import androidx.camera.video.Recorder import androidx.camera.video.VideoCapture import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.util.ComposePermissionManager import java.util.concurrent.ExecutorService import java.util.concurrent.Executors
ImportOrdering:ContentPickerLauncher.kt$import android.app.Activity import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.core.content.FileProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.core.domain.Archive import net.opendasharchive.openarchive.core.domain.Evidence import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.core.repositories.ProjectRepository import net.opendasharchive.openarchive.core.repositories.MediaRepository import net.opendasharchive.openarchive.features.main.ui.AppRoute import net.opendasharchive.openarchive.features.main.ui.Navigator import org.koin.compose.koinInject import net.opendasharchive.openarchive.features.media.camera.CameraActivity import net.opendasharchive.openarchive.features.media.camera.CameraConfig import net.opendasharchive.openarchive.util.Prefs import net.opendasharchive.openarchive.util.Utility import java.io.File import kotlin.coroutines.cancellation.CancellationException
ImportOrdering:ContentPickerSheet.kt$import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.MontserratFontFamily import net.opendasharchive.openarchive.core.presentation.theme.SaveAppTheme
- ImportOrdering:Converters.kt$import androidx.room.TypeConverter import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import net.opendasharchive.openarchive.core.domain.VaultType import net.opendasharchive.openarchive.core.domain.EvidenceStatus import kotlin.time.Instant
+ ImportOrdering:Converters.kt$import androidx.room3.TypeConverter import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import net.opendasharchive.openarchive.core.domain.VaultType import net.opendasharchive.openarchive.core.domain.EvidenceStatus import kotlin.time.Instant
ImportOrdering:CreateNewFolderViewModel.kt$import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.domain.Archive import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.features.core.UiText import net.opendasharchive.openarchive.core.repositories.ProjectRepository import net.opendasharchive.openarchive.core.repositories.SpaceRepository import net.opendasharchive.openarchive.features.core.UiImage import net.opendasharchive.openarchive.features.core.dialog.ButtonData import net.opendasharchive.openarchive.features.core.dialog.DialogConfig import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager import net.opendasharchive.openarchive.features.core.dialog.DialogType import net.opendasharchive.openarchive.features.core.dialog.showSuccessDialog import net.opendasharchive.openarchive.features.main.ui.Navigator import net.opendasharchive.openarchive.util.DateUtils
- ImportOrdering:DatabaseModule.kt$import androidx.room.Room import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.preferencesDataStoreFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.db.AppDatabase import net.opendasharchive.openarchive.core.repositories.* import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named import org.koin.dsl.module
+ ImportOrdering:DatabaseModule.kt$import androidx.room3.Room import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.preferencesDataStoreFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.db.AppDatabase import net.opendasharchive.openarchive.core.repositories.* import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.core.qualifier.named import org.koin.dsl.module
ImportOrdering:DateUtils.kt$import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toInstant import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toKotlinLocalDateTime import java.time.OffsetDateTime import java.util.Date import java.util.Locale import java.time.format.DateTimeFormatter import kotlin.time.Clock import kotlin.time.Instant
ImportOrdering:Evidence.kt$import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable import android.net.Uri import androidx.core.net.toUri import androidx.core.net.toFile import java.io.File
ImportOrdering:FeaturesModule.kt$import android.app.Application import android.content.ContentResolver import net.opendasharchive.openarchive.features.main.ui.HomeViewModel import net.opendasharchive.openarchive.features.main.ui.MainMediaViewModel import net.opendasharchive.openarchive.features.media.PreviewMediaViewModel import net.opendasharchive.openarchive.features.media.ReviewMediaViewModel import net.opendasharchive.openarchive.features.spaces.SpaceListViewModel import net.opendasharchive.openarchive.features.spaces.SpaceSetupViewModel import net.opendasharchive.openarchive.upload.JobSchedulerUploadJobScheduler import net.opendasharchive.openarchive.upload.UploadJobScheduler import net.opendasharchive.openarchive.upload.UploadManagerViewModel import net.opendasharchive.openarchive.features.settings.ProofModeSettingsViewModel import org.koin.android.ext.koin.androidApplication import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module
@@ -2391,7 +2391,7 @@
NoUnusedImports:View.kt$net.opendasharchive.openarchive.util.extensions.View.kt
NoUnusedImports:WebDavLoginScreen.kt$net.opendasharchive.openarchive.services.webdav.presentation.login.WebDavLoginScreen.kt
NoUnusedImports:WebDavLoginViewModel.kt$net.opendasharchive.openarchive.services.webdav.presentation.login.WebDavLoginViewModel.kt
- NoWildcardImports:ArchiveDao.kt$import androidx.room.*
+ NoWildcardImports:ArchiveDao.kt$import androidx.room3.*
NoWildcardImports:ArchiveRepositoryImpl.kt$import kotlinx.coroutines.flow.*
NoWildcardImports:BadgeDrawable.kt$import android.graphics.*
NoWildcardImports:CameraActivity.kt$import androidx.compose.runtime.*
@@ -2407,10 +2407,10 @@
NoWildcardImports:CreativeCommonsLicenseContent.kt$import androidx.compose.material3.*
NoWildcardImports:CreativeCommonsLicenseContent.kt$import androidx.compose.runtime.*
NoWildcardImports:DatabaseModule.kt$import net.opendasharchive.openarchive.core.repositories.*
- NoWildcardImports:DwebDao.kt$import androidx.room.*
+ NoWildcardImports:DwebDao.kt$import androidx.room3.*
NoWildcardImports:DwebRepositoryImpl.kt$import kotlinx.coroutines.flow.*
NoWildcardImports:DwebRepositoryImpl.kt$import net.opendasharchive.openarchive.core.domain.mappers.*
- NoWildcardImports:EvidenceDao.kt$import androidx.room.*
+ NoWildcardImports:EvidenceDao.kt$import androidx.room3.*
NoWildcardImports:EvidenceRepositoryImpl.kt$import kotlinx.coroutines.flow.*
NoWildcardImports:FoldersViewModel.kt$import kotlinx.coroutines.flow.*
NoWildcardImports:Hbks.kt$import java.security.*
@@ -2419,7 +2419,7 @@
NoWildcardImports:IaConduit.kt$import okhttp3.*
NoWildcardImports:Mappers.kt$import net.opendasharchive.openarchive.db.*
NoWildcardImports:Mappers.kt$import net.opendasharchive.openarchive.util.*
- NoWildcardImports:MigrationDao.kt$import androidx.room.*
+ NoWildcardImports:MigrationDao.kt$import androidx.room3.*
NoWildcardImports:MockSnowbirdRepository.kt$import net.opendasharchive.openarchive.db.*
NoWildcardImports:MockSnowbirdRepository.kt$import net.opendasharchive.openarchive.services.snowbird.data.*
NoWildcardImports:QRScanner.kt$import androidx.compose.animation.core.*
@@ -2459,7 +2459,7 @@
NoWildcardImports:SubmissionRepositoryImpl.kt$import kotlinx.coroutines.flow.*
NoWildcardImports:SugarMappers.kt$import net.opendasharchive.openarchive.util.*
NoWildcardImports:UploadService.kt$import android.app.*
- NoWildcardImports:VaultDao.kt$import androidx.room.*
+ NoWildcardImports:VaultDao.kt$import androidx.room3.*
NoWildcardImports:VaultRepositoryImpl.kt$import kotlinx.coroutines.flow.*
PackageName:PasscodeEntryActivity.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
PackageName:PasscodeEntryScreen.kt$package net.opendasharchive.openarchive.features.settings.passcode.passcode_entry
@@ -2809,7 +2809,7 @@
UnusedPrivateProperty:SnowbirdGroupListViewModel.kt$SnowbirdGroupListViewModel$route: AppRoute.SnowbirdGroupListRoute
UnusedPrivateProperty:WebDavLoginViewModel.kt$WebDavLoginViewModel.Companion$private const val REMOTE_PHP_ADDRESS = "/remote.php/webdav/"
UseCheckOrError:ArchiveRepositoryImpl.kt$ArchiveRepositoryImpl$throw IllegalStateException("Project not found")
- WildcardImport:ArchiveDao.kt$import androidx.room.*
+ WildcardImport:ArchiveDao.kt$import androidx.room3.*
WildcardImport:ArchiveRepositoryImpl.kt$import kotlinx.coroutines.flow.*
WildcardImport:BadgeDrawable.kt$import android.graphics.*
WildcardImport:CameraActivity.kt$import androidx.compose.runtime.*
@@ -2825,10 +2825,10 @@
WildcardImport:CreativeCommonsLicenseContent.kt$import androidx.compose.material3.*
WildcardImport:CreativeCommonsLicenseContent.kt$import androidx.compose.runtime.*
WildcardImport:DatabaseModule.kt$import net.opendasharchive.openarchive.core.repositories.*
- WildcardImport:DwebDao.kt$import androidx.room.*
+ WildcardImport:DwebDao.kt$import androidx.room3.*
WildcardImport:DwebRepositoryImpl.kt$import kotlinx.coroutines.flow.*
WildcardImport:DwebRepositoryImpl.kt$import net.opendasharchive.openarchive.core.domain.mappers.*
- WildcardImport:EvidenceDao.kt$import androidx.room.*
+ WildcardImport:EvidenceDao.kt$import androidx.room3.*
WildcardImport:EvidenceRepositoryImpl.kt$import kotlinx.coroutines.flow.*
WildcardImport:FoldersViewModel.kt$import kotlinx.coroutines.flow.*
WildcardImport:Hbks.kt$import java.security.*
@@ -2837,7 +2837,7 @@
WildcardImport:IaConduit.kt$import okhttp3.*
WildcardImport:Mappers.kt$import net.opendasharchive.openarchive.db.*
WildcardImport:Mappers.kt$import net.opendasharchive.openarchive.util.*
- WildcardImport:MigrationDao.kt$import androidx.room.*
+ WildcardImport:MigrationDao.kt$import androidx.room3.*
WildcardImport:MockSnowbirdRepository.kt$import net.opendasharchive.openarchive.db.*
WildcardImport:MockSnowbirdRepository.kt$import net.opendasharchive.openarchive.services.snowbird.data.*
WildcardImport:QRScanner.kt$import androidx.compose.animation.core.*
@@ -2877,7 +2877,7 @@
WildcardImport:SubmissionRepositoryImpl.kt$import kotlinx.coroutines.flow.*
WildcardImport:SugarMappers.kt$import net.opendasharchive.openarchive.util.*
WildcardImport:UploadService.kt$import android.app.*
- WildcardImport:VaultDao.kt$import androidx.room.*
+ WildcardImport:VaultDao.kt$import androidx.room3.*
WildcardImport:VaultRepositoryImpl.kt$import kotlinx.coroutines.flow.*
Wrapping:BaseButton.kt$(
Wrapping:BaseDialog.kt$(
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 97beeb71..eebd2562 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-activity = "1.12.4"
+activity = "1.13.0"
agp = "9.1.0"
androidx-exifinterface = "1.4.2"
androidx-security-crypto = "1.1.0"
@@ -10,25 +10,25 @@ bitcoinj-core = "0.16.2"
bouncycastle-bcpg = "1.71"
bouncycastle-bcpkix = "1.72"
bouncycastle-bcprov = "1.72"
-camerax = "1.6.0-rc01"
+camerax = "1.6.0"
clean-insights = "2.8.0"
coil = "3.4.0"
-compose = "1.11.0-alpha06"
-compose-preference = "2.1.1"
+compose = "1.11.0-beta02"
+compose-preference = "2.2.0"
compose-stability = "0.7.0"
constraintlayout = "2.2.1"
constraintlayout-compos = "1.1.1"
coordinatorlayout = "1.3.0"
-core-ktx = "1.17.0"
+core-ktx = "1.18.0"
core-splashscreen = "1.2.0"
-datastore = "1.3.0-alpha06"
+datastore = "1.3.0-alpha07"
coroutines = "1.10.2"
detekt = "1.23.8"
detekt-compose = "0.5.6"
detekt-rules-compose = "1.4.0"
dotsindicator = "5.1.0"
espresso-core = "3.5.1"
-firebase-analytics = "23.0.0"
+firebase-analytics = "23.2.0"
firebase-crashlytics = "20.0.4"
fragment = "1.8.9"
google-api-client-android = "1.26.0"
@@ -48,21 +48,21 @@ jsoup = "1.17.2"
jtorctl = "0.4.5.7"
junit = "4.13.2"
junit-android = "1.3.0"
-koin = "4.2.0-RC1"
-koin-plugin = "0.3.0"
-kotlin = "2.3.20-RC2"
+koin = "4.2.0"
+koin-plugin = "0.4.1"
+kotlin = "2.3.20"
kotlinx-collections-immutable = "0.4.0"
kotlinx-datetime = "0.7.1"
ksp = "2.3.6"
lifecycle = "2.10.0"
material = "1.13.0"
-material3 = "1.5.0-alpha15"
+material3 = "1.5.0-alpha16"
material-adaptive = "1.2.0"
mlkit-barcode = "17.3.0"
-media3 = "1.9.2"
-mixpanel = "8.3.0"
+media3 = "1.9.3"
+mixpanel = "8.4.0"
navigation = "2.9.7"
-navigation3 = "1.1.0-alpha05"
+navigation3 = "1.1.0-rc01"
navigation-events = "1.1.0-alpha01"
accompanist = "0.37.3"
netcipher = "2.2.0-alpha"
@@ -77,14 +77,14 @@ recyclerview-selection = "1.2.0"
reorderable = "3.0.0"
retrofit = "3.0.0"
robolectric = "4.16.1"
-room = "2.8.4"
+room = "3.0.0-alpha02"
satyan-sugar = "1.5"
serialization = "1.10.0"
swiperefreshlayout = "1.2.0"
timber = "5.0.1"
-tor-android = "0.4.9.5"
+tor-android = "0.4.9.5.1"
viewpager2 = "1.1.0"
-work = "2.11.1"
+work = "2.11.2"
zxing-core = "3.5.4"
zxing-android-embedded = "4.3.0"
@@ -163,9 +163,9 @@ androidx-datastore-preferences = { group = "androidx.datastore", name = "datasto
androidx-work = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
# AndroidX - Room
-androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
-androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
-androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
+androidx-room3-runtime = { group = "androidx.room3", name = "room3-runtime", version.ref = "room" }
+androidx-room3-ktx = { group = "androidx.room3", name = "room3-ktx", version.ref = "room" }
+androidx-room3-compiler = { group = "androidx.room3", name = "room3-compiler", version.ref = "room" }
# AndroidX - Testing
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit-android" }
@@ -227,6 +227,8 @@ koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmo
koin-compose-viewmodel-navigation = { group = "io.insert-koin", name = "koin-compose-viewmodel-navigation", version.ref = "koin" }
koin-compose-navigation3 = { group = "io.insert-koin", name = "koin-compose-navigation3", version.ref = "koin" }
+koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin" }
+
# Kotlin
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
@@ -272,7 +274,7 @@ tor-android = { group = "info.guardianproject", name = "tor-android", version.re
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
-androidx-room = { id = "androidx.room", version.ref = "room" }
+androidx-room3 = { id = "androidx.room3", version.ref = "room" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
detekt-plugin = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "google-firebase-crashlytics" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1f51fe8e..32e52279 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip
\ No newline at end of file
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip
\ No newline at end of file