From 304fa0cc9de9650462208d5e2327b321bdd7430d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 04:06:27 +0000 Subject: [PATCH 1/3] Initial plan From aa0a1e01dd875ae47966bee6c2850e66061fbc6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 04:15:54 +0000 Subject: [PATCH 2/3] Add updateDateTaken method for iOS, macOS, and Android Co-authored-by: AlexV525 <15884415+AlexV525@users.noreply.github.com> --- CHANGELOG.md | 3 + .../photo_manager/constant/Methods.kt | 1 + .../photo_manager/core/PhotoManager.kt | 10 +++ .../photo_manager/core/PhotoManagerPlugin.kt | 11 ++++ .../photo_manager/core/utils/DBUtils.kt | 11 ++++ .../photo_manager/core/utils/IDBUtils.kt | 2 + .../Sources/photo_manager/PMPlugin.m | 10 +++ .../Sources/photo_manager/core/PMManager.h | 2 + .../Sources/photo_manager/core/PMManager.m | 32 ++++++++++ lib/src/internal/constants.dart | 1 + lib/src/internal/editor.dart | 63 +++++++++++++++++++ lib/src/internal/plugin.dart | 11 ++++ 12 files changed, 157 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f15239..8ced3d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ To know more about breaking changes, see the [Migration Guide][]. - Add optional `latitude`, `longitude`, and `creationDate` parameters to `saveImage`, `saveImageWithPath`, and `saveVideo` methods. - On iOS: Sets location and creation date metadata for saved assets. - On Android Q+: Sets DATE_TAKEN field and location metadata for saved assets. +- Add `updateDateTaken` method to `DarwinEditor` and `AndroidEditor` for modifying asset creation time. + - On iOS/macOS: Updates the `creationDate` property of the asset. + - On Android Q (API 29)+: Updates the `DATE_TAKEN` field in MediaStore. ### Improvements diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt index 18fed638..d9deb653 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/constant/Methods.kt @@ -86,6 +86,7 @@ class Methods { const val moveAssetToPath = "moveAssetToPath" const val removeNoExistsAssets = "removeNoExistsAssets" const val getColumnNames = "getColumnNames" + const val updateDateTaken = "updateDateTaken" private val needMediaLocationMethods = arrayOf( getLatLng, diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt index e22cd575..2c55d67a 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt @@ -255,6 +255,16 @@ class PhotoManager(private val context: Context) { } } + fun updateDateTaken(assetId: String, timestamp: Int, resultHandler: ResultHandler) { + try { + val result = dbUtils.updateDateTaken(context, assetId, timestamp) + resultHandler.reply(result) + } catch (e: Exception) { + LogUtils.error(e) + resultHandler.reply(false) + } + } + fun removeAllExistsAssets(resultHandler: ResultHandler) { val result = dbUtils.removeAllExistsAssets(context) resultHandler.reply(result) diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt index dd1508a5..40cd3de3 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt @@ -572,6 +572,17 @@ class PhotoManagerPlugin( favoriteManager.favoriteAsset(photoManager.getUri(assetId), isFavorite, resultHandler) } + Methods.updateDateTaken -> { + val assetId = call.argument("id")!! + val timestamp = call.argument("timestamp")!! + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + LogUtils.error("The API 29 or lower do not support updating DATE_TAKEN.") + resultHandler.reply(false) + return + } + photoManager.updateDateTaken(assetId, timestamp, resultHandler) + } + Methods.copyAsset -> { val assetId = call.argument("assetId")!! val galleryId = call.argument("galleryId")!! diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt index 7a9265b7..5cf4ca99 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt @@ -379,6 +379,17 @@ object DBUtils : IDBUtils { throwMsg("Cannot update $assetId relativePath") } + override fun updateDateTaken(context: Context, assetId: String, timestamp: Int): Boolean { + val cr = context.contentResolver + val timestampMillis = timestamp.toLong() * 1000 + val contentValues = ContentValues().apply { + put(MediaStore.Images.ImageColumns.DATE_TAKEN, timestampMillis) + } + + val count = cr.update(allUri, contentValues, idSelection, arrayOf(assetId)) + return count > 0 + } + private val deleteLock = ReentrantLock() override fun removeAllExistsAssets(context: Context): Boolean { diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt index ec670597..ed1d2e25 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt @@ -579,6 +579,8 @@ interface IDBUtils { fun moveToGallery(context: Context, assetId: String, galleryId: String): AssetEntity + fun updateDateTaken(context: Context, assetId: String, timestamp: Int): Boolean + fun getSomeInfo(context: Context, assetId: String): Pair? fun getUri(id: Long, type: Int, isOrigin: Boolean = false): Uri { diff --git a/darwin/photo_manager/Sources/photo_manager/PMPlugin.m b/darwin/photo_manager/Sources/photo_manager/PMPlugin.m index 1993bd2a..76a3810d 100644 --- a/darwin/photo_manager/Sources/photo_manager/PMPlugin.m +++ b/darwin/photo_manager/Sources/photo_manager/PMPlugin.m @@ -693,6 +693,16 @@ - (void)handleMethodResultHandler:(PMResultHandler *)handler manager:(PMManager [handler reply:@(result)]; } }]; + } else if ([@"updateDateTaken" isEqualToString:call.method]) { + NSString *id = call.arguments[@"id"]; + NSNumber *timestamp = call.arguments[@"timestamp"]; + [manager updateDateTakenWithId:id timestamp:timestamp block:^(BOOL result, NSObject *error) { + if (error) { + [handler replyError:error]; + } else { + [handler reply:@(result)]; + } + }]; } else if ([@"requestCacheAssetsThumb" isEqualToString:call.method]) { NSArray *ids = call.arguments[@"ids"]; PMThumbLoadOption *option = [PMThumbLoadOption optionDict:call.arguments[@"option"]]; diff --git a/darwin/photo_manager/Sources/photo_manager/core/PMManager.h b/darwin/photo_manager/Sources/photo_manager/core/PMManager.h index 85ece3d8..acd62eb2 100644 --- a/darwin/photo_manager/Sources/photo_manager/core/PMManager.h +++ b/darwin/photo_manager/Sources/photo_manager/core/PMManager.h @@ -126,6 +126,8 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *); - (void)favoriteWithId:(NSString *)id favorite:(BOOL)favorite block:(void (^)(BOOL result, NSObject *))block; +- (void)updateDateTakenWithId:(NSString *)id timestamp:(NSNumber *)timestamp block:(void (^)(BOOL result, NSObject *))block; + - (void)clearFileCache; - (void)requestCacheAssetsThumb:(NSArray *)ids option:(PMThumbLoadOption *)option; diff --git a/darwin/photo_manager/Sources/photo_manager/core/PMManager.m b/darwin/photo_manager/Sources/photo_manager/core/PMManager.m index c8a4e0d4..5716fa0c 100644 --- a/darwin/photo_manager/Sources/photo_manager/core/PMManager.m +++ b/darwin/photo_manager/Sources/photo_manager/core/PMManager.m @@ -1880,6 +1880,38 @@ - (void)favoriteWithId:(NSString *)id favorite:(BOOL)favorite block:(void (^)(BO block(YES, nil); } +- (void)updateDateTakenWithId:(NSString *)id timestamp:(NSNumber *)timestamp block:(void (^)(BOOL result, NSObject *error))block { + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:[self singleFetchOptions]]; + PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult]; + if (!asset) { + block(NO, [NSString stringWithFormat:@"Asset %@ not found.", id]); + return; + } + + if (![asset canPerformEditOperation:PHAssetEditOperationProperties]) { + block(NO, [NSString stringWithFormat:@"The asset %@ cannot perform edit operation.", id]); + return; + } + + NSTimeInterval timeInterval = [timestamp doubleValue]; + NSDate *newDate = [NSDate dateWithTimeIntervalSince1970:timeInterval]; + + NSError *error; + BOOL succeed = [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ + PHAssetChangeRequest *request = [PHAssetChangeRequest changeRequestForAsset:asset]; + request.creationDate = newDate; + } error:&error]; + if (!succeed) { + block(NO, [NSString stringWithFormat:@"Updating creation date for asset %@ failed: Request not succeed.", id]); + return; + } + if (error) { + block(NO, error); + return; + } + block(YES, nil); +} + - (NSString *)getCachePath:(NSString *)type { NSString *homePath = NSTemporaryDirectory(); NSString *cachePath = type; diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index b71eed8c..f2d8ed70 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -60,6 +60,7 @@ class PMConstants { static const String mRemoveInAlbum = 'removeInAlbum'; static const String mMoveAssetToPath = 'moveAssetToPath'; static const String mColumnNames = 'getColumnNames'; + static const String mUpdateDateTaken = 'updateDateTaken'; static const String mGetAssetCount = 'getAssetCount'; static const String mGetAssetsByRange = 'getAssetsByRange'; diff --git a/lib/src/internal/editor.dart b/lib/src/internal/editor.dart index c41f60b3..3f5ba9a3 100644 --- a/lib/src/internal/editor.dart +++ b/lib/src/internal/editor.dart @@ -311,6 +311,37 @@ class DarwinEditor { ); } + /// Updates the creation date of the given [entity]. + /// + /// This method modifies the creation date metadata of the asset. + /// On iOS/macOS, this updates the `creationDate` property of the asset. + /// + /// Returns the updated [AssetEntity] with the new creation date if the operation + /// was successful; otherwise, throws a [StateError]. + /// + /// Example: + /// ```dart + /// final newDate = DateTime(2023, 1, 15, 10, 30); + /// final updatedAsset = await PhotoManager().editor.darwin.updateDateTaken( + /// entity: myAsset, + /// dateTime: newDate, + /// ); + /// ``` + Future updateDateTaken({ + required AssetEntity entity, + required DateTime dateTime, + }) async { + final bool result = await plugin.updateDateTaken(entity.id, dateTime); + if (result) { + final int timestampInSeconds = dateTime.millisecondsSinceEpoch ~/ 1000; + return entity.copyWith(createDateSecond: timestampInSeconds); + } + throw StateError( + 'Failed to update date taken for asset ' + '${entity.id}', + ); + } + /// Save Live Photo to the gallery from the given [imageFile] and [videoFile]. /// /// {@macro photo_manager.Editor.TitleWhenSaving} @@ -359,6 +390,38 @@ class AndroidEditor { ); } + /// Updates the creation date of the given [entity]. + /// + /// This method modifies the creation date metadata of the asset. + /// On Android Q (API 29) and above, this updates the `DATE_TAKEN` field in MediaStore. + /// On Android P (API 28) and below, this method is not supported. + /// + /// Returns the updated [AssetEntity] with the new creation date if the operation + /// was successful; otherwise, throws a [StateError]. + /// + /// Example: + /// ```dart + /// final newDate = DateTime(2023, 1, 15, 10, 30); + /// final updatedAsset = await PhotoManager().editor.android.updateDateTaken( + /// entity: myAsset, + /// dateTime: newDate, + /// ); + /// ``` + Future updateDateTaken({ + required AssetEntity entity, + required DateTime dateTime, + }) async { + final bool result = await plugin.updateDateTaken(entity.id, dateTime); + if (result) { + final int timestampInSeconds = dateTime.millisecondsSinceEpoch ~/ 1000; + return entity.copyWith(createDateSecond: timestampInSeconds); + } + throw StateError( + 'Failed to update date taken for asset ' + '${entity.id}', + ); + } + /// Moves the given [entity] to the specified [target] path. /// /// Returns `true` if the move was successful; otherwise, `false`. diff --git a/lib/src/internal/plugin.dart b/lib/src/internal/plugin.dart index 141181d3..5b6de455 100644 --- a/lib/src/internal/plugin.dart +++ b/lib/src/internal/plugin.dart @@ -599,6 +599,17 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin, OhosPlugin { return result == true; } + Future updateDateTaken(String id, DateTime dateTime) async { + final bool? result = await _channel.invokeMethod( + PMConstants.mUpdateDateTaken, + { + 'id': id, + 'timestamp': dateTime.millisecondsSinceEpoch ~/ 1000, + }, + ); + return result == true; + } + Future ignorePermissionCheck(bool ignore) { return _channel.invokeMethod( PMConstants.mIgnorePermissionCheck, From da1bb4fe984eb6be46e66d950dab4fedc8f00612 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 04:18:16 +0000 Subject: [PATCH 3/3] Fix timestamp type to Long and use correct DATE_TAKEN import Co-authored-by: AlexV525 <15884415+AlexV525@users.noreply.github.com> --- .../com/fluttercandies/photo_manager/core/PhotoManager.kt | 2 +- .../fluttercandies/photo_manager/core/PhotoManagerPlugin.kt | 2 +- .../com/fluttercandies/photo_manager/core/utils/DBUtils.kt | 6 +++--- .../com/fluttercandies/photo_manager/core/utils/IDBUtils.kt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt index 2c55d67a..78e15cce 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManager.kt @@ -255,7 +255,7 @@ class PhotoManager(private val context: Context) { } } - fun updateDateTaken(assetId: String, timestamp: Int, resultHandler: ResultHandler) { + fun updateDateTaken(assetId: String, timestamp: Long, resultHandler: ResultHandler) { try { val result = dbUtils.updateDateTaken(context, assetId, timestamp) resultHandler.reply(result) diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt index 40cd3de3..188dbdb6 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/PhotoManagerPlugin.kt @@ -574,7 +574,7 @@ class PhotoManagerPlugin( Methods.updateDateTaken -> { val assetId = call.argument("id")!! - val timestamp = call.argument("timestamp")!! + val timestamp = call.argument("timestamp")!! if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { LogUtils.error("The API 29 or lower do not support updating DATE_TAKEN.") resultHandler.reply(false) diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt index 5cf4ca99..ed782cb2 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/DBUtils.kt @@ -379,11 +379,11 @@ object DBUtils : IDBUtils { throwMsg("Cannot update $assetId relativePath") } - override fun updateDateTaken(context: Context, assetId: String, timestamp: Int): Boolean { + override fun updateDateTaken(context: Context, assetId: String, timestamp: Long): Boolean { val cr = context.contentResolver - val timestampMillis = timestamp.toLong() * 1000 + val timestampMillis = timestamp * 1000 val contentValues = ContentValues().apply { - put(MediaStore.Images.ImageColumns.DATE_TAKEN, timestampMillis) + put(DATE_TAKEN, timestampMillis) } val count = cr.update(allUri, contentValues, idSelection, arrayOf(assetId)) diff --git a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt index ed1d2e25..1b286286 100644 --- a/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt +++ b/android/src/main/kotlin/com/fluttercandies/photo_manager/core/utils/IDBUtils.kt @@ -579,7 +579,7 @@ interface IDBUtils { fun moveToGallery(context: Context, assetId: String, galleryId: String): AssetEntity - fun updateDateTaken(context: Context, assetId: String, timestamp: Int): Boolean + fun updateDateTaken(context: Context, assetId: String, timestamp: Long): Boolean fun getSomeInfo(context: Context, assetId: String): Pair?