#1427 increased precision of file modified date to milliseconds

This commit is contained in:
Thibault Deckers 2025-02-16 20:32:34 +01:00
parent 9280b4a6a7
commit 5f26cfbbf3
25 changed files with 153 additions and 96 deletions

View file

@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
### Changed
- increased precision of file modified date to milliseconds
- upgraded Flutter to stable v3.29.0
### Fixed

View file

@ -8,6 +8,7 @@ import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.utils.MimeTypes
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -34,11 +35,11 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
}
private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")
val mimeType = call.argument<String>("mimeType")
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong()
val rotationDegrees = call.argument<Int>("rotationDegrees")
val isFlipped = call.argument<Boolean>("isFlipped")
val uri = call.argument<String>(EntryFields.URI)
val mimeType = call.argument<String>(EntryFields.MIME_TYPE)
val dateModifiedMillis = call.argument<Number>(EntryFields.DATE_MODIFIED_MILLIS)?.toLong()
val rotationDegrees = call.argument<Int>(EntryFields.ROTATION_DEGREES)
val isFlipped = call.argument<Boolean>(EntryFields.IS_FLIPPED)
val widthDip = call.argument<Number>("widthDip")?.toDouble()
val heightDip = call.argument<Number>("heightDip")?.toDouble()
val pageId = call.argument<Int>("pageId")
@ -55,7 +56,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
context = context,
uri = uri,
mimeType = mimeType,
dateModifiedSecs = dateModifiedSecs ?: (Date().time / 1000),
dateModifiedMillis = dateModifiedMillis ?: (Date().time),
rotationDegrees = rotationDegrees,
isFlipped = isFlipped,
width = (widthDip * density).roundToInt(),

View file

@ -30,7 +30,7 @@ class ThumbnailFetcher internal constructor(
private val context: Context,
uri: String,
private val mimeType: String,
private val dateModifiedSecs: Long,
private val dateModifiedMillis: Long,
private val rotationDegrees: Int,
private val isFlipped: Boolean,
width: Int?,
@ -119,7 +119,7 @@ class ThumbnailFetcher internal constructor(
// add signature to ignore cache for images which got modified but kept the same URI
var options = RequestOptions()
.format(if (quality == 100) DecodeFormat.PREFER_ARGB_8888 else DecodeFormat.PREFER_RGB_565)
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId"))
.signature(ObjectKey("$dateModifiedMillis-$rotationDegrees-$isFlipped-$width-$pageId"))
.override(width, height)
if (isVideo(mimeType)) {
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE)

View file

@ -19,12 +19,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private lateinit var eventSink: EventSink
private lateinit var handler: Handler
private var knownEntries: Map<Long?, Int?>? = null
// knownEntries: map of contentId -> dateModifiedMillis
private var knownEntries: Map<Long?, Long?>? = null
private var directory: String? = null
init {
if (arguments is Map<*, *>) {
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to (it.value as Number?)?.toLong() }?.toMap()
directory = arguments["directory"] as String?
}
}

View file

@ -18,7 +18,7 @@ object EntryFields {
const val IS_FLIPPED = "isFlipped" // boolean
const val DATE_ADDED_SECS = "dateAddedSecs" // long
const val DATE_MODIFIED_SECS = "dateModifiedSecs" // long
const val DATE_MODIFIED_MILLIS = "dateModifiedMillis" // long
const val SOURCE_DATE_TAKEN_MILLIS = "sourceDateTakenMillis" // long
const val DURATION_MILLIS = "durationMillis" // long

View file

@ -42,7 +42,7 @@ class SourceEntry {
private var sourceRotationDegrees: Int? = null
private var sizeBytes: Long? = null
private var dateAddedSecs: Long? = null
private var dateModifiedSecs: Long? = null
private var dateModifiedMillis: Long? = null
private var sourceDateTakenMillis: Long? = null
private var durationMillis: Long? = null
@ -65,16 +65,16 @@ class SourceEntry {
sizeBytes = toLong(map[EntryFields.SIZE_BYTES])
title = map[EntryFields.TITLE] as String?
dateAddedSecs = toLong(map[EntryFields.DATE_ADDED_SECS])
dateModifiedSecs = toLong(map[EntryFields.DATE_MODIFIED_SECS])
dateModifiedMillis = toLong(map[EntryFields.DATE_MODIFIED_MILLIS])
sourceDateTakenMillis = toLong(map[EntryFields.SOURCE_DATE_TAKEN_MILLIS])
durationMillis = toLong(map[EntryFields.DURATION_MILLIS])
}
fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedSecs: Long) {
fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedMillis: Long) {
this.path = path
this.title = title
this.sizeBytes = sizeBytes
this.dateModifiedSecs = dateModifiedSecs
this.dateModifiedMillis = dateModifiedMillis
}
fun toMap(): FieldMap {
@ -89,7 +89,7 @@ class SourceEntry {
EntryFields.SIZE_BYTES to sizeBytes,
EntryFields.TITLE to title,
EntryFields.DATE_ADDED_SECS to dateAddedSecs,
EntryFields.DATE_MODIFIED_SECS to dateModifiedSecs,
EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
EntryFields.SOURCE_DATE_TAKEN_MILLIS to sourceDateTakenMillis,
EntryFields.DURATION_MILLIS to durationMillis,
// only for map export

View file

@ -46,7 +46,7 @@ internal class FileImageProvider : ImageProvider() {
path = path,
title = file.name,
sizeBytes = file.length(),
dateModifiedSecs = file.lastModified() / 1000,
dateModifiedMillis = file.lastModified(),
)
}
} catch (e: SecurityException) {
@ -91,7 +91,7 @@ internal class FileImageProvider : ImageProvider() {
return hashMapOf(
EntryFields.URI to Uri.fromFile(newFile).toString(),
EntryFields.PATH to newFile.path,
EntryFields.DATE_MODIFIED_SECS to newFile.lastModified() / 1000,
EntryFields.DATE_MODIFIED_MILLIS to newFile.lastModified(),
)
}
@ -99,7 +99,7 @@ internal class FileImageProvider : ImageProvider() {
try {
val file = File(path)
if (file.exists()) {
newFields[EntryFields.DATE_MODIFIED_SECS] = file.lastModified() / 1000
newFields[EntryFields.DATE_MODIFIED_MILLIS] = file.lastModified()
newFields[EntryFields.SIZE_BYTES] = file.length()
}
callback.onSuccess(newFields)

View file

@ -51,14 +51,14 @@ import kotlin.coroutines.suspendCoroutine
class MediaStoreImageProvider : ImageProvider() {
fun fetchAll(
context: Context,
knownEntries: Map<Long?, Int?>,
knownEntries: Map<Long?, Long?>,
directory: String?,
handleNewEntry: NewEntryHandler,
) {
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory")
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
val isModified = fun(contentId: Long, dateModifiedMillis: Long): Boolean {
val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs
return knownDate == null || knownDate < dateModifiedMillis
}
val handleNew: NewEntryHandler
var selection: String? = null
@ -96,7 +96,7 @@ class MediaStoreImageProvider : ImageProvider() {
var found = false
val fetched = arrayListOf<FieldMap>()
val id = uri.tryParseId()
val alwaysValid: NewEntryChecker = fun(_: Long, _: Int): Boolean = true
val alwaysValid: NewEntryChecker = fun(_: Long, _: Long): Boolean = true
val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
if (id != null) {
if (sourceMimeType == null || isImage(sourceMimeType)) {
@ -227,8 +227,8 @@ class MediaStoreImageProvider : ImageProvider() {
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val dateAddedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN)
// image & video for API >=29, only for images for API <29
@ -240,8 +240,8 @@ class MediaStoreImageProvider : ImageProvider() {
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val dateModifiedSecs = cursor.getInt(dateModifiedColumn)
if (isValidEntry(id, dateModifiedSecs)) {
val dateModifiedMillis = cursor.getInt(dateModifiedSecsColumn) * 1000L
if (isValidEntry(id, dateModifiedMillis)) {
// for multiple items, `contentUri` is the root without ID,
// but for single items, `contentUri` already contains the ID
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, id)
@ -255,17 +255,18 @@ class MediaStoreImageProvider : ImageProvider() {
if (mimeType == null) {
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else {
var entryMap: FieldMap = hashMapOf(
val path = cursor.getString(pathColumn)
var entryFields: FieldMap = hashMapOf(
EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
EntryFields.URI to itemUri.toString(),
EntryFields.PATH to cursor.getString(pathColumn),
EntryFields.PATH to path,
EntryFields.SOURCE_MIME_TYPE to mimeType,
EntryFields.WIDTH to width,
EntryFields.HEIGHT to height,
EntryFields.SOURCE_ROTATION_DEGREES to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
EntryFields.SIZE_BYTES to cursor.getLong(sizeColumn),
EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedColumn),
EntryFields.DATE_MODIFIED_SECS to dateModifiedSecs,
EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedSecsColumn),
EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
EntryFields.SOURCE_DATE_TAKEN_MILLIS to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
EntryFields.DURATION_MILLIS to durationMillis,
// only for map export
@ -285,8 +286,8 @@ class MediaStoreImageProvider : ImageProvider() {
if (outWidth > 0 && outHeight > 0) {
width = outWidth
height = outHeight
entryMap[EntryFields.WIDTH] = width
entryMap[EntryFields.HEIGHT] = height
entryFields[EntryFields.WIDTH] = width
entryFields[EntryFields.HEIGHT] = height
}
}
} catch (e: IOException) {
@ -302,11 +303,13 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
entryMap = entry.toMap()
val entry = SourceEntry(entryFields).fillPreCatalogMetadata(context)
entryFields = entry.toMap()
}
handleNewEntry(entryMap)
getFileModifiedDateMillis(path)?.let { entryFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
handleNewEntry(entryFields)
found = true
}
}
@ -823,18 +826,32 @@ class MediaStoreImageProvider : ImageProvider() {
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields["sizeBytes"] = cursor.getLong(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_MILLIS] = cursor.getInt(it) * 1000 }
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields[EntryFields.SIZE_BYTES] = cursor.getLong(it) }
cursor.close()
}
} catch (e: Exception) {
callback.onFailure(e)
return@scanFile
}
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
callback.onSuccess(newFields)
}
}
// try to fetch the modified date from the file,
// as it is more precise than the one from the Media Store
private fun getFileModifiedDateMillis(path: String?): Long? {
if (path != null) {
try {
return File(path).lastModified()
} catch (securityException: SecurityException) {
// ignore
}
}
return null
}
private fun scanObsoletePath(context: Context, uri: Uri, path: String, mimeType: String) {
val file = File(path)
val delayMillis = 500L
@ -918,8 +935,9 @@ class MediaStoreImageProvider : ImageProvider() {
EntryFields.PATH to path,
)
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields[EntryFields.DATE_ADDED_SECS] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_SECS] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_MILLIS] = cursor.getInt(it) * 1000 }
cursor.close()
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
return newFields
}
} catch (e: Exception) {
@ -1030,4 +1048,4 @@ object MediaColumns {
typealias NewEntryHandler = (entry: FieldMap) -> Unit
private typealias NewEntryChecker = (contentId: Long, dateModifiedSecs: Int) -> Boolean
private typealias NewEntryChecker = (contentId: Long, dateModifiedMillis: Long) -> Boolean

View file

@ -42,7 +42,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
pageId: pageId,
rotationDegrees: key.rotationDegrees,
isFlipped: key.isFlipped,
dateModifiedSecs: key.dateModifiedSecs,
dateModifiedMillis: key.dateModifiedMillis,
extent: key.extent,
taskKey: key,
);
@ -75,11 +75,11 @@ class ThumbnailProviderKey extends Equatable {
final int? pageId;
final int rotationDegrees;
final bool isFlipped;
final int dateModifiedSecs;
final int dateModifiedMillis;
final double extent;
@override
List<Object?> get props => [uri, pageId, dateModifiedSecs, extent];
List<Object?> get props => [uri, pageId, dateModifiedMillis, extent];
const ThumbnailProviderKey({
required this.uri,
@ -87,10 +87,10 @@ class ThumbnailProviderKey extends Equatable {
required this.pageId,
required this.rotationDegrees,
required this.isFlipped,
required this.dateModifiedSecs,
required this.dateModifiedMillis,
this.extent = 0,
});
@override
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent}';
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedMillis=$dateModifiedMillis, extent=$extent}';
}

View file

@ -57,7 +57,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
', sizeBytes INTEGER'
', title TEXT'
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
', dateModifiedSecs INTEGER'
', dateModifiedMillis INTEGER'
', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER'
', trashed INTEGER DEFAULT 0'
@ -117,7 +117,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
')');
},
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
version: 13,
version: 14,
);
final maxIdRows = await _db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');

View file

@ -47,6 +47,8 @@ class LocalMediaDbUpgrader {
await _upgradeFrom11(db);
case 12:
await _upgradeFrom12(db);
case 13:
await _upgradeFrom13(db);
}
oldVersion++;
}
@ -444,4 +446,37 @@ class LocalMediaDbUpgrader {
await db.execute('ALTER TABLE $newCoverTable RENAME TO $coverTable;');
});
}
static Future<void> _upgradeFrom13(Database db) async {
debugPrint('upgrading DB from v13');
// rename column 'dateModifiedSecs' to 'dateModifiedMillis'
await db.transaction((txn) async {
const newEntryTable = '${entryTable}TEMP';
await db.execute('CREATE TABLE $newEntryTable('
'id INTEGER PRIMARY KEY'
', contentId INTEGER'
', uri TEXT'
', path TEXT'
', sourceMimeType TEXT'
', width INTEGER'
', height INTEGER'
', sourceRotationDegrees INTEGER'
', sizeBytes INTEGER'
', title TEXT'
', dateAddedSecs INTEGER DEFAULT (strftime(\'%s\',\'now\'))'
', dateModifiedMillis INTEGER'
', sourceDateTakenMillis INTEGER'
', durationMillis INTEGER'
', trashed INTEGER DEFAULT 0'
', origin INTEGER DEFAULT 0'
')');
await db.rawInsert('INSERT INTO $newEntryTable(contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin)'
' SELECT contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedSecs*1000,sourceDateTakenMillis,durationMillis,trashed,origin'
' FROM $entryTable;');
await db.execute('DROP TABLE $entryTable;');
await db.execute('ALTER TABLE $newEntryTable RENAME TO $entryTable;');
});
}
}

View file

@ -19,11 +19,11 @@ class EntryCache {
static Future<void> evict(
String uri,
String mimeType,
int? dateModifiedSecs,
int? dateModifiedMillis,
int rotationDegrees,
bool isFlipped,
) async {
debugPrint('Evict cached images for uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped');
debugPrint('Evict cached images for uri=$uri, mimeType=$mimeType, dateModifiedMillis=$dateModifiedMillis, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped');
// TODO TLAD provide pageId parameter for multi page items, if someday image editing features are added for them
int? pageId;
@ -42,7 +42,7 @@ class EntryCache {
uri: uri,
mimeType: mimeType,
pageId: pageId,
dateModifiedSecs: dateModifiedSecs ?? 0,
dateModifiedMillis: dateModifiedMillis ?? 0,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
)).evict();
@ -53,7 +53,7 @@ class EntryCache {
uri: uri,
mimeType: mimeType,
pageId: pageId,
dateModifiedSecs: dateModifiedSecs ?? 0,
dateModifiedMillis: dateModifiedMillis ?? 0,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
extent: extent,

View file

@ -36,7 +36,7 @@ class AvesEntry with AvesEntryBase {
int? contentId;
final String sourceMimeType;
int width, height, sourceRotationDegrees;
int? dateAddedSecs, _dateModifiedSecs, sourceDateTakenMillis, _durationMillis;
int? dateAddedSecs, _dateModifiedMillis, sourceDateTakenMillis, _durationMillis;
bool trashed;
int origin;
@ -66,7 +66,7 @@ class AvesEntry with AvesEntryBase {
required this.sizeBytes,
required String? sourceTitle,
required this.dateAddedSecs,
required int? dateModifiedSecs,
required int? dateModifiedMillis,
required this.sourceDateTakenMillis,
required int? durationMillis,
required this.trashed,
@ -82,7 +82,7 @@ class AvesEntry with AvesEntryBase {
}
this.path = path;
this.sourceTitle = sourceTitle;
this.dateModifiedSecs = dateModifiedSecs;
this.dateModifiedMillis = dateModifiedMillis;
this.durationMillis = durationMillis;
}
@ -93,7 +93,7 @@ class AvesEntry with AvesEntryBase {
int? contentId,
String? title,
int? dateAddedSecs,
int? dateModifiedSecs,
int? dateModifiedMillis,
int? origin,
List<AvesEntry>? stackedEntries,
}) {
@ -111,7 +111,7 @@ class AvesEntry with AvesEntryBase {
sizeBytes: sizeBytes,
sourceTitle: title ?? sourceTitle,
dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs,
dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs,
dateModifiedMillis: dateModifiedMillis ?? this.dateModifiedMillis,
sourceDateTakenMillis: sourceDateTakenMillis,
durationMillis: durationMillis,
trashed: trashed,
@ -140,7 +140,7 @@ class AvesEntry with AvesEntryBase {
sizeBytes: map[EntryFields.sizeBytes] as int?,
sourceTitle: map[EntryFields.title] as String?,
dateAddedSecs: map[EntryFields.dateAddedSecs] as int?,
dateModifiedSecs: map[EntryFields.dateModifiedSecs] as int?,
dateModifiedMillis: map[EntryFields.dateModifiedMillis] as int?,
sourceDateTakenMillis: map[EntryFields.sourceDateTakenMillis] as int?,
durationMillis: map[EntryFields.durationMillis] as int?,
trashed: (map[EntryFields.trashed] as int? ?? 0) != 0,
@ -162,7 +162,7 @@ class AvesEntry with AvesEntryBase {
EntryFields.sizeBytes: sizeBytes,
EntryFields.title: sourceTitle,
EntryFields.dateAddedSecs: dateAddedSecs,
EntryFields.dateModifiedSecs: dateModifiedSecs,
EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sourceDateTakenMillis: sourceDateTakenMillis,
EntryFields.durationMillis: durationMillis,
EntryFields.trashed: trashed ? 1 : 0,
@ -180,7 +180,7 @@ class AvesEntry with AvesEntryBase {
EntryFields.height: height,
EntryFields.rotationDegrees: rotationDegrees,
EntryFields.isFlipped: isFlipped,
EntryFields.dateModifiedSecs: dateModifiedSecs,
EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sizeBytes: sizeBytes,
EntryFields.trashed: trashed,
EntryFields.trashPath: trashDetails?.path,
@ -241,7 +241,7 @@ class AvesEntry with AvesEntryBase {
DateTime? _bestDate;
DateTime? get bestDate {
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis((dateModifiedSecs ?? 0) * 1000);
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis(dateModifiedMillis ?? 0);
return _bestDate;
}
@ -292,10 +292,10 @@ class AvesEntry with AvesEntryBase {
_bestTitle = null;
}
int? get dateModifiedSecs => _dateModifiedSecs;
int? get dateModifiedMillis => _dateModifiedMillis;
set dateModifiedSecs(int? dateModifiedSecs) {
_dateModifiedSecs = dateModifiedSecs;
set dateModifiedMillis(int? dateModifiedMillis) {
_dateModifiedMillis = dateModifiedMillis;
_bestDate = null;
}
@ -362,7 +362,7 @@ class AvesEntry with AvesEntryBase {
set catalogMetadata(CatalogMetadata? newMetadata) {
final oldMimeType = mimeType;
final oldDateModifiedSecs = dateModifiedSecs;
final oldDateModifiedMillis = dateModifiedMillis;
final oldRotationDegrees = rotationDegrees;
final oldIsFlipped = isFlipped;
@ -372,7 +372,7 @@ class AvesEntry with AvesEntryBase {
_tags = null;
metadataChangeNotifier.notify();
_onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
_onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
}
void clearMetadata() {
@ -399,7 +399,7 @@ class AvesEntry with AvesEntryBase {
Future<void> applyNewFields(Map newFields, {required bool persist}) async {
final oldMimeType = mimeType;
final oldDateModifiedSecs = this.dateModifiedSecs;
final oldDateModifiedMillis = this.dateModifiedMillis;
final oldRotationDegrees = this.rotationDegrees;
final oldIsFlipped = this.isFlipped;
@ -426,8 +426,8 @@ class AvesEntry with AvesEntryBase {
final sizeBytes = newFields[EntryFields.sizeBytes];
if (sizeBytes is int) this.sizeBytes = sizeBytes;
final dateModifiedSecs = newFields[EntryFields.dateModifiedSecs];
if (dateModifiedSecs is int) this.dateModifiedSecs = dateModifiedSecs;
final dateModifiedMillis = newFields[EntryFields.dateModifiedMillis];
if (dateModifiedMillis is int) this.dateModifiedMillis = dateModifiedMillis;
final rotationDegrees = newFields[EntryFields.rotationDegrees];
if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
final isFlipped = newFields[EntryFields.isFlipped];
@ -438,7 +438,7 @@ class AvesEntry with AvesEntryBase {
if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!});
}
await _onVisualFieldChanged(oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
await _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
metadataChangeNotifier.notify();
}
@ -479,12 +479,12 @@ class AvesEntry with AvesEntryBase {
// when the MIME type or the image itself changed (e.g. after rotation)
Future<void> _onVisualFieldChanged(
String oldMimeType,
int? oldDateModifiedSecs,
int? oldDateModifiedMillis,
int oldRotationDegrees,
bool oldIsFlipped,
) async {
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedSecs != dateModifiedSecs || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
await EntryCache.evict(uri, oldMimeType, oldDateModifiedSecs, oldRotationDegrees, oldIsFlipped);
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedMillis != dateModifiedMillis || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
await EntryCache.evict(uri, oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
visualChangeNotifier.notify();
}
}

View file

@ -25,7 +25,7 @@ extension ExtraAvesEntryImages on AvesEntry {
pageId: pageId,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
dateModifiedSecs: dateModifiedSecs ?? -1,
dateModifiedMillis: dateModifiedMillis ?? -1,
extent: requestExtent,
);
}

View file

@ -17,7 +17,7 @@ class EntryFields {
static const isFlipped = 'isFlipped'; // boolean
static const dateAddedSecs = 'dateAddedSecs'; // long
static const dateModifiedSecs = 'dateModifiedSecs'; // long
static const dateModifiedMillis = 'dateModifiedMillis'; // long
static const sourceDateTakenMillis = 'sourceDateTakenMillis'; // long
static const durationMillis = 'durationMillis'; // long

View file

@ -120,7 +120,7 @@ class MultiPageInfo {
sizeBytes: mainEntry.sizeBytes,
sourceTitle: mainEntry.sourceTitle,
dateAddedSecs: mainEntry.dateAddedSecs,
dateModifiedSecs: mainEntry.dateModifiedSecs,
dateModifiedMillis: mainEntry.dateModifiedMillis,
sourceDateTakenMillis: mainEntry.sourceDateTakenMillis,
durationMillis: pageInfo.durationMillis ?? mainEntry.durationMillis,
trashed: trashed,

View file

@ -245,10 +245,10 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
switch (key) {
case EntryFields.contentId:
entry.contentId = newValue as int?;
case EntryFields.dateModifiedSecs:
// `dateModifiedSecs` changes when moving entries to another directory,
case EntryFields.dateModifiedMillis:
// `dateModifiedMillis` changes when moving entries to another directory,
// but it does not change when renaming the containing directory
entry.dateModifiedSecs = newValue as int?;
entry.dateModifiedMillis = newValue as int?;
case EntryFields.path:
entry.path = newValue as String?;
case EntryFields.title:
@ -369,7 +369,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
// title can change when moved files are automatically renamed to avoid conflict
title: newFields[EntryFields.title] as String?,
dateAddedSecs: newFields[EntryFields.dateAddedSecs] as int?,
dateModifiedSecs: newFields[EntryFields.dateModifiedSecs] as int?,
dateModifiedMillis: newFields[EntryFields.dateModifiedMillis] as int?,
origin: newFields[EntryFields.origin] as int?,
));
} else {

View file

@ -101,7 +101,7 @@ class MediaStoreSource extends CollectionSource {
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs)));
final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedMillis)));
final knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
if (topEntries.isNotEmpty) {
@ -290,7 +290,7 @@ class MediaStoreSource extends CollectionSource {
if (sourceEntry != null) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
// compare paths because some apps move files without updating their `last modified date`
if (existingEntry == null || (sourceEntry.dateModifiedSecs ?? 0) > (existingEntry.dateModifiedSecs ?? 0) || sourceEntry.path != existingEntry.path) {
if (existingEntry == null || (sourceEntry.dateModifiedMillis ?? 0) > (existingEntry.dateModifiedMillis ?? 0) || sourceEntry.path != existingEntry.path) {
final newPath = sourceEntry.path;
final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
if (volume != null) {

View file

@ -225,7 +225,7 @@ class PlatformAppService implements AppService {
pageId: coverEntry.pageId,
rotationDegrees: coverEntry.rotationDegrees,
isFlipped: coverEntry.isFlipped,
dateModifiedSecs: coverEntry.dateModifiedSecs,
dateModifiedMillis: coverEntry.dateModifiedMillis,
extent: size,
);
}

View file

@ -53,7 +53,7 @@ abstract class MediaFetchService {
required int rotationDegrees,
required int? pageId,
required bool isFlipped,
required int? dateModifiedSecs,
required int? dateModifiedMillis,
required double extent,
Object? taskKey,
int? priority,
@ -211,7 +211,7 @@ class PlatformMediaFetchService implements MediaFetchService {
required int rotationDegrees,
required int? pageId,
required bool isFlipped,
required int? dateModifiedSecs,
required int? dateModifiedMillis,
required double extent,
Object? taskKey,
int? priority,
@ -222,7 +222,7 @@ class PlatformMediaFetchService implements MediaFetchService {
final result = await _platformBytes.invokeMethod('getThumbnail', <String, dynamic>{
'uri': uri,
'mimeType': mimeType,
'dateModifiedSecs': dateModifiedSecs,
'dateModifiedMillis': dateModifiedMillis,
'rotationDegrees': rotationDegrees,
'isFlipped': isFlipped,
'widthDip': extent,

View file

@ -14,7 +14,7 @@ abstract class MediaStoreService {
Future<int?> getGeneration();
// knownEntries: map of contentId -> dateModifiedSecs
// knownEntries: map of contentId -> dateModifiedMillis
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory});
// returns media URI

View file

@ -109,7 +109,7 @@ class _EditorImageState extends State<EditorImage> {
final viewportSize = margin.deflateSize(constraints.biggest);
final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize));
return AvesMagnifier(
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
controller: widget.magnifierController,
viewportPadding: margin,
contentSize: mediaSize,

View file

@ -98,7 +98,7 @@ class ViewerDebugPage extends StatelessWidget {
info: {
'catalogDateMillis': toDateValue(entry.catalogDateMillis),
'dateAddedSecs': toDateValue(entry.dateAddedSecs, factor: 1000),
'dateModifiedSecs': toDateValue(entry.dateModifiedSecs, factor: 1000),
'dateModifiedMillis': toDateValue(entry.dateModifiedMillis),
'sourceDateTakenMillis': toDateValue(entry.sourceDateTakenMillis),
'bestDate': '${entry.bestDate}',
},

View file

@ -398,7 +398,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
return AvesMagnifier(
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
controller: controller ?? _magnifierController,
contentSize: displaySize ?? entry.displaySize,
allowOriginalScaleBeyondRange: !isWallpaperMode,

View file

@ -40,12 +40,13 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
static int get nextId => _lastId++;
static int get dateSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000;
static int get dateMillis => DateTime.now().millisecondsSinceEpoch;
static AvesEntry newImage(String album, String filenameWithoutExtension, {int? id, int? contentId}) {
id ??= nextId;
contentId ??= id;
final date = dateSecs;
final _dateMillis = dateMillis;
final _dateSecs = _dateMillis ~/ 1000;
return AvesEntry(
origin: EntryOrigins.mediaStoreContent,
id: id,
@ -59,9 +60,9 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
sourceRotationDegrees: 0,
sizeBytes: 42,
sourceTitle: filenameWithoutExtension,
dateAddedSecs: date,
dateModifiedSecs: date,
sourceDateTakenMillis: date,
dateAddedSecs: _dateSecs,
dateModifiedMillis: _dateMillis,
sourceDateTakenMillis: _dateMillis,
durationMillis: null,
trashed: false,
);
@ -77,7 +78,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
EntryFields.uri: 'content://media/external/images/media/$newContentId',
EntryFields.contentId: newContentId,
EntryFields.path: entry.path!.replaceFirst(sourceAlbum, destinationAlbum),
EntryFields.dateModifiedSecs: FakeMediaStoreService.dateSecs,
EntryFields.dateModifiedMillis: FakeMediaStoreService.dateMillis,
},
deleted: false,
);
@ -94,7 +95,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
EntryFields.uri: 'content://media/external/images/media/$newContentId',
EntryFields.contentId: newContentId,
EntryFields.path: entry.path!.replaceFirst(oldName, newName),
EntryFields.dateModifiedSecs: FakeMediaStoreService.dateSecs,
EntryFields.dateModifiedMillis: FakeMediaStoreService.dateMillis,
},
deleted: false,
);