diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MediaManager.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MediaManager.kt index 48238ae29d..7d57c13789 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MediaManager.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MediaManager.kt @@ -44,6 +44,14 @@ class MediaManager(context: Context) { } } + fun clearSyncCheckpoint() { + ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit().apply { + remove(SHARED_PREF_MEDIA_STORE_VERSION_KEY) + remove(SHARED_PREF_MEDIA_STORE_GEN_KEY) + apply() + } + } + @RequiresApi(Build.VERSION_CODES.Q) fun getAssetIdsForAlbum(albumId: String): List { val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt index b3c83f1846..4553609350 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt @@ -192,6 +192,7 @@ interface ImHostService { fun shouldFullSync(): Boolean fun getMediaChanges(): SyncDelta fun checkpointSync() + fun clearSyncCheckpoint() fun getAssetIdsForAlbum(albumId: String): List companion object { @@ -250,6 +251,22 @@ interface ImHostService { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.clearSyncCheckpoint$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.clearSyncCheckpoint() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) if (api != null) { diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MessagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MessagesImpl.kt index 556ff8e2d7..c61c688273 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MessagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MessagesImpl.kt @@ -37,6 +37,10 @@ class MessagesImpl(context: Context) : ImHostService { mediaManager.checkpointSync() } + override fun clearSyncCheckpoint() { + mediaManager.clearSyncCheckpoint() + } + override fun getAssetIdsForAlbum(albumId: String): List { if (!isMediaChangesSupported()) { throw unsupportedFeatureException() diff --git a/mobile/ios/Runner/Platform/MediaManager.swift b/mobile/ios/Runner/Platform/MediaManager.swift index 362113c12b..4d5b19c022 100644 --- a/mobile/ios/Runner/Platform/MediaManager.swift +++ b/mobile/ios/Runner/Platform/MediaManager.swift @@ -50,6 +50,11 @@ class MediaManager { } } + func clearSyncCheckpoint() -> Void { + defaults.removeObject(forKey: changeTokenKey) + print("MediaManager::removeChangeToken: Change token removed from UserDefaults") + } + @available(iOS 16, *) func checkpointSync() { saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken) @@ -150,9 +155,15 @@ class MediaManager { let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] albumTypes.forEach { type in - let collections = PHAssetCollection.fetchAssetCollectionsContaining(forAsset, with: type, options: nil) + let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) collections.enumerateObjects { (album, _, _) in - albumIds.append(album.localIdentifier) + var options = PHFetchOptions() + options.fetchLimit = 1 + options.predicate = NSPredicate(format: "localIdentifier == %@", forAsset.localIdentifier) + let result = PHAsset.fetchAssets(in: album, options: options) + if(result.count == 1) { + albumIds.append(album.localIdentifier) + } } } return albumIds diff --git a/mobile/ios/Runner/Platform/Messages.g.swift b/mobile/ios/Runner/Platform/Messages.g.swift index 2db2d3d7c7..8f9a71340c 100644 --- a/mobile/ios/Runner/Platform/Messages.g.swift +++ b/mobile/ios/Runner/Platform/Messages.g.swift @@ -256,6 +256,7 @@ protocol ImHostService { func shouldFullSync() throws -> Bool func getMediaChanges() throws -> SyncDelta func checkpointSync() throws + func clearSyncCheckpoint() throws func getAssetIdsForAlbum(albumId: String) throws -> [String] } @@ -311,6 +312,19 @@ class ImHostServiceSetup { } else { checkpointSyncChannel.setMessageHandler(nil) } + let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + clearSyncCheckpointChannel.setMessageHandler { _, reply in + do { + try api.clearSyncCheckpoint() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + clearSyncCheckpointChannel.setMessageHandler(nil) + } let getAssetIdsForAlbumChannel = taskQueue == nil ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) diff --git a/mobile/ios/Runner/Platform/MessagesImpl.swift b/mobile/ios/Runner/Platform/MessagesImpl.swift index ea1282d88c..c0bf245998 100644 --- a/mobile/ios/Runner/Platform/MessagesImpl.swift +++ b/mobile/ios/Runner/Platform/MessagesImpl.swift @@ -1,6 +1,7 @@ import Photos class ImHostServiceImpl: ImHostService { + private let mediaManager: MediaManager init() { @@ -29,6 +30,10 @@ class ImHostServiceImpl: ImHostService { } } + func clearSyncCheckpoint() { + mediaManager.clearSyncCheckpoint() + } + func getAssetIdsForAlbum(albumId: String) throws -> [String] { // Android specific, empty list is safe no-op return [] diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 8f0419f756..849091b357 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -24,7 +24,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository final assetCount = _db.localAlbumAssetEntity.assetId.count(); final query = _db.localAlbumEntity.select().join([ - innerJoin( + leftOuterJoin( _db.localAlbumAssetEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), useColumns: false, diff --git a/mobile/lib/platform/messages.dart b/mobile/lib/platform/messages.dart index a364c99a66..b535a95d84 100644 --- a/mobile/lib/platform/messages.dart +++ b/mobile/lib/platform/messages.dart @@ -53,6 +53,8 @@ abstract class ImHostService { void checkpointSync(); + void clearSyncCheckpoint(); + @TaskQueue(type: TaskQueueType.serialBackgroundThread) List getAssetIdsForAlbum(String albumId); } diff --git a/mobile/lib/platform/messages.g.dart b/mobile/lib/platform/messages.g.dart index 8e2b51f0b0..590c7cb8fd 100644 --- a/mobile/lib/platform/messages.g.dart +++ b/mobile/lib/platform/messages.g.dart @@ -275,6 +275,29 @@ class ImHostService { } } + Future clearSyncCheckpoint() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + Future> getAssetIdsForAlbum(String albumId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( diff --git a/mobile/lib/presentation/pages/feat_in_development.page.dart b/mobile/lib/presentation/pages/feat_in_development.page.dart index f61e1956f7..6bacd9049d 100644 --- a/mobile/lib/presentation/pages/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/feat_in_development.page.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; +import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/routing/router.dart'; final _features = [ @@ -25,6 +27,21 @@ final _features = [ .read(driftProvider) .customStatement("pragma wal_checkpoint(truncate)"), ), + _Feature( + name: 'Clear Delta Checkpoint', + icon: Icons.delete_rounded, + onTap: (_, ref) => ref.read(hostServiceProvider).clearSyncCheckpoint(), + ), + _Feature( + name: 'Clear Local Data', + icon: Icons.delete_forever_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.localAssetEntity.deleteAll(); + await db.localAlbumEntity.deleteAll(); + await db.localAlbumAssetEntity.deleteAll(); + }, + ), _Feature( name: 'Local Media Summary', icon: Icons.table_chart_rounded, diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart index 5dc4f4eb48..7b85e52829 100644 --- a/mobile/lib/providers/infrastructure/platform.provider.dart +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -1,4 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/platform/messages.g.dart'; -final platformMessagesImpl = Provider((_) => ImHostService()); +final hostServiceProvider = Provider((_) => ImHostService()); diff --git a/mobile/lib/providers/infrastructure/sync.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart index a4d85ba7e2..ff91cb0f23 100644 --- a/mobile/lib/providers/infrastructure/sync.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -13,7 +13,7 @@ final deviceSyncServiceProvider = Provider( (ref) => DeviceSyncService( albumMediaRepository: ref.watch(albumMediaRepositoryProvider), localAlbumRepository: ref.watch(localAlbumRepository), - hostService: ref.watch(platformMessagesImpl), + hostService: ref.watch(hostServiceProvider), ), );