Compare commits

..

2 Commits

Author SHA1 Message Date
shenlong-tanwen e701ba7788 fix: wrap migrations in transaction 2026-07-07 03:37:56 +05:30
Santo Shakil 58b1c5e8fb fix(mobile): fix 10-bit heic and avif colors on android (#29631)
* fix(mobile): fix 10-bit heic and avif colors on android

* fix(mobile): share the decode pool across flutter engines

* vectorize conversion

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2026-07-06 21:16:09 +00:00
7 changed files with 352 additions and 201 deletions
@@ -107,3 +107,67 @@ Java_app_alextran_immich_NativeImage_rotate(
} }
return (jlong) dst; return (jlong) dst;
} }
// Convert an RGBA_1010102 buffer to densely-packed RGBA_8888, matching Skia's
// Bitmap.copy(ARGB_8888) byte-for-byte so it's a drop-in for the intermediate 8888 bitmap.
// Each source pixel is a native (little-endian on every Android ABI) u32 with R in bits 0-9,
// G in 10-19, B in 20-29, A in 30-31 (standard RGB10_A2). Each 10-bit channel maps to 8-bit via
// round(v*255/1023). The 2-bit alpha maps to a*85. Both are kept as plain arithmetic as the whole
// loop auto-vectorizes to NEON and measures faster than a LUT on-device. Output is R,G,B,A bytes
// per pixel, i.e. Android ARGB_8888 memory == Dart PixelFormat.rgba8888.
static void convert_1010102(const uint8_t *src, int srcStride, uint32_t *dst, int w, int h) {
for (int y = 0; y < h; y++) {
const uint32_t *srcRow = (const uint32_t *) (src + (size_t) y * srcStride);
uint32_t *dstRow = dst + (size_t) y * w;
for (int x = 0; x < w; x++) {
uint32_t px = srcRow[x];
uint32_t r = ((px & 0x3FF) * 16336u + 32768u) >> 16;
uint32_t g = (((px >> 10) & 0x3FF) * 16336u + 32768u) >> 16;
uint32_t b = (((px >> 20) & 0x3FF) * 16336u + 32768u) >> 16;
uint32_t a = ((px >> 30) & 0x3) * 85u;
dstRow[x] = r | (g << 8) | (b << 16) | (a << 24);
}
}
}
// Converts an RGBA_1010102 bitmap (what a 10-bit HEIC/AVIF decodes to on API 33+) into a freshly
// malloc'd RGBA_8888 buffer. Fills outInfo with {width, height, rowBytes} and returns the buffer
// address, or 0 (so the caller falls back to a Skia copy) if the bitmap isn't 1010102 or can't be
// locked. Same ownership contract as rotate: free the returned buffer via NativeBuffer.free.
JNIEXPORT jlong JNICALL
Java_app_alextran_immich_NativeImage_convert1010102(
JNIEnv *env, jclass clazz, jobject bitmap, jintArray outInfo) {
AndroidBitmapInfo info;
if (AndroidBitmap_getInfo(env, bitmap, &info) != ANDROID_BITMAP_RESULT_SUCCESS) {
return 0;
}
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_1010102) {
return 0;
}
int w = (int) info.width;
int h = (int) info.height;
uint32_t *dst = (uint32_t *) malloc((size_t) w * h * 4);
if (dst == NULL) {
return 0;
}
void *srcPixels = NULL;
if (AndroidBitmap_lockPixels(env, bitmap, &srcPixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
free(dst);
return 0;
}
convert_1010102((const uint8_t *) srcPixels, (int) info.stride, dst, w, h);
AndroidBitmap_unlockPixels(env, bitmap);
jint dims[3] = {w, h, w * 4};
(*env)->SetIntArrayRegion(env, outInfo, 0, 3, dims);
if ((*env)->ExceptionCheck(env)) {
free(dst);
return 0;
}
return (jlong) dst;
}
@@ -16,4 +16,14 @@ object NativeImage {
*/ */
@JvmStatic @JvmStatic
external fun rotate(bitmap: Bitmap, orientation: Int, outInfo: IntArray): Long external fun rotate(bitmap: Bitmap, orientation: Int, outInfo: IntArray): Long
/**
* Converts an RGBA_1010102 [bitmap] (what a 10-bit HEIC/AVIF decodes to on API 33+) to RGBA_8888,
* writing the result into a freshly malloc'd native buffer in one pass, with no intermediate
* ARGB_8888 bitmap. Returns the buffer address (free it with [NativeBuffer.free]) and fills
* [outInfo] with {width, height, rowBytes}. Returns 0 when the bitmap isn't RGBA_1010102 so the
* caller can fall back to a Skia copy.
*/
@JvmStatic
external fun convert1010102(bitmap: Bitmap, outInfo: IntArray): Long
} }
@@ -46,22 +46,47 @@ inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap {
} }
fun Bitmap.toNativeBuffer(): Map<String, Long> { fun Bitmap.toNativeBuffer(): Map<String, Long> {
val size = width * height * 4 // Dart reads the buffer as rgba8888, but 10-bit sources decode to RGBA_1010102, which garbles
// colors when copied verbatim. Convert those straight into the output buffer in native code -
// one pass, no intermediate ARGB_8888 bitmap.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && config == Bitmap.Config.RGBA_1010102) {
val info = IntArray(3)
val pointer = NativeImage.convert1010102(this, info)
if (pointer != 0L) {
recycle()
return mapOf(
"pointer" to pointer,
"width" to info[0].toLong(),
"height" to info[1].toLong(),
"rowBytes" to info[2].toLong()
)
}
// native convert declined (OOM/lock) -> fall through to the Skia copy path below.
}
// Other non-8888 configs (e.g. HDR F16) still need Skia's convert; 8-bit is copied as-is.
val bitmap = if (config != Bitmap.Config.ARGB_8888) {
val converted = copy(Bitmap.Config.ARGB_8888, false)
recycle()
converted ?: throw IOException("could not convert bitmap to ARGB_8888")
} else {
this
}
val size = bitmap.width * bitmap.height * 4
val pointer = NativeBuffer.allocate(size) val pointer = NativeBuffer.allocate(size)
try { try {
val buffer = NativeBuffer.wrap(pointer, size) val buffer = NativeBuffer.wrap(pointer, size)
copyPixelsToBuffer(buffer) bitmap.copyPixelsToBuffer(buffer)
return mapOf( return mapOf(
"pointer" to pointer, "pointer" to pointer,
"width" to width.toLong(), "width" to bitmap.width.toLong(),
"height" to height.toLong(), "height" to bitmap.height.toLong(),
"rowBytes" to (width * 4).toLong() "rowBytes" to (bitmap.width * 4).toLong()
) )
} catch (e: Exception) { } catch (e: Throwable) {
NativeBuffer.free(pointer) NativeBuffer.free(pointer)
throw e throw e
} finally { } finally {
recycle() bitmap.recycle()
} }
} }
@@ -1,6 +1,8 @@
package app.alextran.immich.images package app.alextran.immich.images
import android.content.Context import android.content.Context
import android.graphics.ImageDecoder
import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import android.os.OperationCanceledException import android.os.OperationCanceledException
import app.alextran.immich.INITIAL_BUFFER_SIZE import app.alextran.immich.INITIAL_BUFFER_SIZE
@@ -22,6 +24,7 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024 private const val MAX_PREALLOC_BYTES = 128 * 1024 * 1024
@@ -36,12 +39,16 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
companion object { companion object {
val CANCELLED = Result.success<Map<String, Long>?>(null) val CANCELLED = Result.success<Map<String, Long>?>(null)
// Shared, process-lifetime pool: RemoteImagesImpl is re-created per FlutterEngine, so a
// per-instance pool would leak threads across engine restarts.
private val decodeExecutor = Executors.newFixedThreadPool(2)
} }
override fun requestImage( override fun requestImage(
url: String, url: String,
requestId: Long, requestId: Long,
@Suppress("UNUSED_PARAMETER") preferEncoded: Boolean, // always returns encoded; setting has no effect on Android preferEncoded: Boolean,
callback: (Result<Map<String, Long>?>) -> Unit callback: (Result<Map<String, Long>?>) -> Unit
) { ) {
val signal = CancellationSignal() val signal = CancellationSignal()
@@ -51,12 +58,51 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
url, url,
signal, signal,
onSuccess = { buffer -> onSuccess = { buffer ->
requestMap.remove(requestId)
if (signal.isCanceled) { if (signal.isCanceled) {
NativeBuffer.free(buffer.pointer) requestMap.remove(requestId)
buffer.free()
return@fetch callback(CANCELLED) return@fetch callback(CANCELLED)
} }
// Decode natively when the caller wants pixels: Flutter's fallback decoder copies
// 10-bit bitmaps (RGBA_1010102) as if they were rgba8888, garbling colors. Decode on a
// dedicated pool - the fetch callback threads are shared with video streaming. On any
// decode failure (including OOM on huge originals), hand Flutter the encoded bytes as
// before.
if (!preferEncoded && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
decodeExecutor.execute {
val res = if (signal.isCanceled) null else try {
val source = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset))
source.decodeBitmap().toNativeBuffer()
} catch (_: Throwable) {
null
}
requestMap.remove(requestId)
when {
// Deliver even if the request was cancelled meanwhile: re-checking here would orphan
// res's malloc, and Dart frees the buffer itself when it sees the cancel.
res != null -> {
buffer.free()
callback(Result.success(res))
}
signal.isCanceled -> {
buffer.free()
callback(CANCELLED)
}
else -> callback(
Result.success(
mapOf(
"pointer" to buffer.pointer,
"length" to buffer.offset.toLong()
)
)
)
}
}
return@fetch
}
requestMap.remove(requestId)
callback( callback(
Result.success( Result.success(
mapOf( mapOf(
@@ -70,8 +70,7 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1" enableGPUValidationMode = "1"
allowLocationSimulation = "YES" allowLocationSimulation = "YES">
queueDebuggingEnabled = "NO">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
@@ -12,7 +12,7 @@ class RemoteImageRequest extends ImageRequest {
} }
final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false); final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false);
// Android always returns encoded data, so we need to check for both shapes of the response. // Android falls back to encoded data if native decoding fails, so check for both shapes of the response.
final frame = switch (info) { final frame = switch (info) {
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length), {'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} => {'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
@@ -128,199 +128,206 @@ class Drift extends $Drift {
// Run migration steps without foreign keys and re-enable them later // Run migration steps without foreign keys and re-enable them later
await customStatement('PRAGMA foreign_keys = OFF'); await customStatement('PRAGMA foreign_keys = OFF');
await m.runMigrationSteps( try {
from: from, await transaction(
to: to, () => m.runMigrationSteps(
steps: migrationSteps( from: from,
from1To2: (m, v2) async { to: to,
for (final entity in v2.entities) { steps: migrationSteps(
await m.drop(entity); from1To2: (m, v2) async {
await m.create(entity); for (final entity in v2.entities) {
} await m.drop(entity);
}, await m.create(entity);
from2To3: (m, v3) async { }
// Removed foreign key constraint on stack.primaryAssetId },
await m.alterTable(TableMigration(v3.stackEntity)); from2To3: (m, v3) async {
}, // Removed foreign key constraint on stack.primaryAssetId
from3To4: (m, v4) async { await m.alterTable(TableMigration(v3.stackEntity));
// Thumbnail path column got removed from person_entity },
await m.alterTable(TableMigration(v4.personEntity)); from3To4: (m, v4) async {
// asset_face_entity is added // Thumbnail path column got removed from person_entity
await m.create(v4.assetFaceEntity); await m.alterTable(TableMigration(v4.personEntity));
}, // asset_face_entity is added
from4To5: (m, v5) async { await m.create(v4.assetFaceEntity);
await m.alterTable( },
TableMigration( from4To5: (m, v5) async {
v5.userEntity, await m.alterTable(
newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt], TableMigration(
columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime}, v5.userEntity,
), newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt],
); columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime},
}, ),
from5To6: (m, v6) async { );
// Drops the (checksum, ownerId) and adds it back as (ownerId, checksum) },
await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum'); from5To6: (m, v6) async {
await m.drop(v6.idxRemoteAssetOwnerChecksum); // Drops the (checksum, ownerId) and adds it back as (ownerId, checksum)
await m.create(v6.idxRemoteAssetOwnerChecksum); await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum');
// Adds libraryId to remote_asset_entity await m.drop(v6.idxRemoteAssetOwnerChecksum);
await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId); await m.create(v6.idxRemoteAssetOwnerChecksum);
await m.drop(v6.uQRemoteAssetsOwnerChecksum); // Adds libraryId to remote_asset_entity
await m.create(v6.uQRemoteAssetsOwnerChecksum); await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId);
await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum); await m.drop(v6.uQRemoteAssetsOwnerChecksum);
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum); await m.create(v6.uQRemoteAssetsOwnerChecksum);
}, await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum);
from6To7: (m, v7) async { await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
await m.createIndex(v7.idxLatLng); },
}, from6To7: (m, v7) async {
from7To8: (m, v8) async { await m.createIndex(v7.idxLatLng);
await m.create(v8.storeEntity); },
}, from7To8: (m, v8) async {
from8To9: (m, v9) async { await m.create(v8.storeEntity);
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId); },
}, from8To9: (m, v9) async {
from9To10: (m, v10) async { await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
await m.createTable(v10.authUserEntity); },
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor); from9To10: (m, v10) async {
await m.alterTable(TableMigration(v10.userEntity)); await m.createTable(v10.authUserEntity);
}, await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
from10To11: (m, v11) async { await m.alterTable(TableMigration(v10.userEntity));
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_); },
}, from10To11: (m, v11) async {
from11To12: (m, v12) async { await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
final localToUTCMapping = { },
v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt], from11To12: (m, v12) async {
v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt], final localToUTCMapping = {
}; v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt],
v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt],
};
for (final entry in localToUTCMapping.entries) { for (final entry in localToUTCMapping.entries) {
final table = entry.key; final table = entry.key;
await m.alterTable( await m.alterTable(
TableMigration( TableMigration(
table, table,
columnTransformer: { columnTransformer: {
for (final column in entry.value) for (final column in entry.value)
column: column.modify(const DateTimeModifier.utc()).strftime('%Y-%m-%dT%H:%M:%fZ'), column: column.modify(const DateTimeModifier.utc()).strftime('%Y-%m-%dT%H:%M:%fZ'),
}, },
), ),
); );
} }
}, },
from12To13: (m, v13) async { from12To13: (m, v13) async {
await m.create(v13.trashedLocalAssetEntity); await m.create(v13.trashedLocalAssetEntity);
await m.createIndex(v13.idxTrashedLocalAssetChecksum); await m.createIndex(v13.idxTrashedLocalAssetChecksum);
await m.createIndex(v13.idxTrashedLocalAssetAlbum); await m.createIndex(v13.idxTrashedLocalAssetAlbum);
}, },
from13To14: (m, v14) async { from13To14: (m, v14) async {
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime); await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime);
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude); await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude); await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
}, },
from14To15: (m, v15) async { from14To15: (m, v15) async {
await m.alterTable( await m.alterTable(
TableMigration( TableMigration(
v15.trashedLocalAssetEntity, v15.trashedLocalAssetEntity,
columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)}, columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)},
newColumns: [v15.trashedLocalAssetEntity.source], newColumns: [v15.trashedLocalAssetEntity.source],
), ),
); );
}, },
from15To16: (m, v16) async { from15To16: (m, v16) async {
// Add i_cloud_id to local and remote asset tables // Add i_cloud_id to local and remote asset tables
await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId); await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId);
await m.createIndex(v16.idxLocalAssetCloudId); await m.createIndex(v16.idxLocalAssetCloudId);
await m.createTable(v16.remoteAssetCloudIdEntity); await m.createTable(v16.remoteAssetCloudIdEntity);
}, },
from16To17: (m, v17) async { from16To17: (m, v17) async {
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited); await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
}, },
from17To18: (m, v18) async { from17To18: (m, v18) async {
await m.createIndex(v18.idxRemoteAssetCloudId); await m.createIndex(v18.idxRemoteAssetCloudId);
}, },
from18To19: (m, v19) async { from18To19: (m, v19) async {
await m.createIndex(v19.idxAssetFacePersonId); await m.createIndex(v19.idxAssetFacePersonId);
await m.createIndex(v19.idxAssetFaceAssetId); await m.createIndex(v19.idxAssetFaceAssetId);
await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset); await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset);
await m.createIndex(v19.idxPartnerSharedWithId); await m.createIndex(v19.idxPartnerSharedWithId);
await m.createIndex(v19.idxPersonOwnerId); await m.createIndex(v19.idxPersonOwnerId);
await m.createIndex(v19.idxRemoteAlbumOwnerId); await m.createIndex(v19.idxRemoteAlbumOwnerId);
await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset); await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset);
await m.createIndex(v19.idxRemoteAssetStackId); await m.createIndex(v19.idxRemoteAssetStackId);
await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay); await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay);
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth); await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
await m.createIndex(v19.idxStackPrimaryAssetId); await m.createIndex(v19.idxStackPrimaryAssetId);
}, },
from19To20: (m, v20) async { from19To20: (m, v20) async {
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible); await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt); await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
}, },
from20To21: (m, v21) async { from20To21: (m, v21) async {
await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle); await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle);
await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle); await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle);
}, },
from21To22: (m, v22) async { from21To22: (m, v22) async {
await m.createTable(v22.assetEditEntity); await m.createTable(v22.assetEditEntity);
await m.createIndex(v22.idxAssetEditAssetId); await m.createIndex(v22.idxAssetEditAssetId);
}, },
from22To23: (m, v23) async { from22To23: (m, v23) async {
await m.renameColumn(v23.localAssetEntity, 'duration_in_seconds', v23.localAssetEntity.durationMs); await m.renameColumn(v23.localAssetEntity, 'duration_in_seconds', v23.localAssetEntity.durationMs);
await m.renameColumn(v23.remoteAssetEntity, 'duration_in_seconds', v23.remoteAssetEntity.durationMs); await m.renameColumn(v23.remoteAssetEntity, 'duration_in_seconds', v23.remoteAssetEntity.durationMs);
await m.renameColumn( await m.renameColumn(
v23.trashedLocalAssetEntity, v23.trashedLocalAssetEntity,
'duration_in_seconds', 'duration_in_seconds',
v23.trashedLocalAssetEntity.durationMs, v23.trashedLocalAssetEntity.durationMs,
); );
await localAssetEntity.update().write( await localAssetEntity.update().write(
LocalAssetEntityCompanion.custom(durationMs: v23.localAssetEntity.durationMs * const Constant(1000)), LocalAssetEntityCompanion.custom(durationMs: v23.localAssetEntity.durationMs * const Constant(1000)),
); );
await remoteAssetEntity.update().write( await remoteAssetEntity.update().write(
RemoteAssetEntityCompanion.custom(durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000)), RemoteAssetEntityCompanion.custom(
); durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000),
await trashedLocalAssetEntity.update().write( ),
TrashedLocalAssetEntityCompanion.custom( );
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000), await trashedLocalAssetEntity.update().write(
), TrashedLocalAssetEntityCompanion.custom(
); durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
}, ),
from23To24: (m, v24) async { );
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id'); },
await m.alterTable(TableMigration(v24.remoteAlbumEntity)); from23To24: (m, v24) async {
}, await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
from24To25: (m, v25) async { await m.alterTable(TableMigration(v24.remoteAlbumEntity));
await m.createTable(v25.metadata); },
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum'); from24To25: (m, v25) async {
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day'); await m.createTable(v25.metadata);
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month'); await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated); await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
await m.createIndex(v25.idxRemoteExifCity); await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
await m.createIndex(v25.idxAssetFaceVisiblePerson); await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
}, await m.createIndex(v25.idxRemoteExifCity);
from25To26: (m, v26) async { await m.createIndex(v25.idxAssetFaceVisiblePerson);
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt); },
}, from25To26: (m, v26) async {
from26To27: (m, v27) async { await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
await customStatement('ALTER TABLE metadata RENAME TO settings'); },
}, from26To27: (m, v27) async {
from27To28: (m, v28) async { await customStatement('ALTER TABLE metadata RENAME TO settings');
await m.createIndex(v28.idxLocalAssetCreatedAt); },
}, from27To28: (m, v28) async {
from28To29: (m, v29) async { await m.createIndex(v28.idxLocalAssetCreatedAt);
await m.createTable(v29.assetOcrEntity); },
await m.createIndex(v29.idxAssetOcrAssetId); from28To29: (m, v29) async {
}, await m.createTable(v29.assetOcrEntity);
from29To30: (m, v30) async { await m.createIndex(v29.idxAssetOcrAssetId);
await m.alterTable(TableMigration(v30.settings)); },
}, from29To30: (m, v30) async {
), await m.alterTable(TableMigration(v30.settings));
); },
),
),
);
if (kDebugMode) { if (kDebugMode) {
// Fail if the migration broke foreign keys // Fail if the migration broke foreign keys
final wrongFKs = await customSelect('PRAGMA foreign_key_check').get(); final wrongFKs = await customSelect('PRAGMA foreign_key_check').get();
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}'); assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
}
} finally {
await customStatement('PRAGMA foreign_keys = ON;');
} }
await customStatement('PRAGMA foreign_keys = ON;');
await optimize(); await optimize();
}, },
beforeOpen: (details) async { beforeOpen: (details) async {