make the native calls synchronous, pass datetime as long

This commit is contained in:
shenlong-tanwen 2025-05-09 11:53:50 +05:30
parent 87599daaac
commit 63ebba671b
9 changed files with 292 additions and 372 deletions

View File

@ -9,9 +9,6 @@ import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension import androidx.annotation.RequiresExtension
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
class MediaManager(context: Context) { class MediaManager(context: Context) {
private val ctx: Context = context.applicationContext private val ctx: Context = context.applicationContext
@ -30,158 +27,140 @@ class MediaManager(context: Context) {
} }
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
fun shouldFullSync(callback: (Result<Boolean>) -> Unit) { fun shouldFullSync(): Boolean {
val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val currVersion = MediaStore.getVersion(ctx) return MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
val lastVersion = prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
callback(Result.success(currVersion != lastVersion))
} }
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1)
fun checkpointSync(callback: (Result<Unit>) -> Unit) { fun checkpointSync() {
val genMap = val genMap =
MediaStore.getExternalVolumeNames(ctx).associateWith { MediaStore.getGeneration(ctx, it) } MediaStore.getExternalVolumeNames(ctx).associateWith { MediaStore.getGeneration(ctx, it) }
val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit().apply {
prefs.edit().apply {
putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx)) putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx))
putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap)) putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap))
apply() apply()
} }
callback(Result.success(Unit))
} }
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) { fun getAssetIdsForAlbum(albumId: String): List<String> {
try { val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val projection = arrayOf(MediaStore.Files.FileColumns._ID) val selection =
val selection = "${MediaStore.Files.FileColumns.BUCKET_ID} = ? AND (${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
"${MediaStore.Files.FileColumns.BUCKET_ID} = ? AND (${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" val selectionArgs = arrayOf(
val selectionArgs = arrayOf( albumId,
albumId, MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString() )
)
val ids = return ctx.contentResolver.query(uri, projection, selection, selectionArgs, null)
ctx.contentResolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor -> ?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
generateSequence { generateSequence {
if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null
}.toList() }.toList()
} ?: emptyList() } ?: emptyList()
callback(Result.success(ids))
} catch (e: Exception) {
callback(Result.failure(e))
}
} }
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1)
fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) { fun getMediaChanges(): SyncDelta {
try { val genMap = getSavedGenerationMap(ctx)
val genMap = getSavedGenerationMap(ctx) val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
val currentVolumes = MediaStore.getExternalVolumeNames(ctx) val changed = mutableListOf<Asset>()
val changed = mutableListOf<Asset>() val deleted = mutableListOf<String>()
val deleted = mutableListOf<String>()
val formatter = DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneOffset.UTC)
var hasChanges = genMap.keys != currentVolumes var hasChanges = genMap.keys != currentVolumes
for (volume in currentVolumes) { for (volume in currentVolumes) {
val currentGen = MediaStore.getGeneration(ctx, volume) val currentGen = MediaStore.getGeneration(ctx, volume)
val storedGen = genMap[volume] val storedGen = genMap[volume]
if (storedGen != null && currentGen <= storedGen) { if (storedGen != null && currentGen <= storedGen) {
continue continue
} }
hasChanges = true hasChanges = true
val uri = MediaStore.Files.getContentUri(volume) val uri = MediaStore.Files.getContentUri(volume)
val projection = arrayOf( val projection = arrayOf(
MediaStore.MediaColumns._ID, MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.DATE_TAKEN, MediaStore.MediaColumns.DATE_TAKEN,
MediaStore.MediaColumns.DATE_ADDED, MediaStore.MediaColumns.DATE_ADDED,
MediaStore.MediaColumns.DATE_MODIFIED, MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.Files.FileColumns.MEDIA_TYPE, MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns.BUCKET_ID, MediaStore.MediaColumns.BUCKET_ID,
MediaStore.MediaColumns.DURATION MediaStore.MediaColumns.DURATION
) )
val selection = val selection =
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?) AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)" "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?) AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)"
val selectionArgs = arrayOf( val selectionArgs = arrayOf(
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString(), MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString(),
storedGen?.toString() ?: "0", storedGen?.toString() ?: "0",
storedGen?.toString() ?: "0" storedGen?.toString() ?: "0"
) )
ctx.contentResolver.query( ctx.contentResolver.query(
uri, uri,
projection, projection,
selection, selection,
selectionArgs, selectionArgs,
null null
)?.use { cursor -> )?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN) val dateTakenColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN)
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn = val dateModifiedColumn =
cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val mediaTypeColumn = val mediaTypeColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
val bucketIdColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID) val bucketIdColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn).toString() val id = cursor.getLong(idColumn).toString()
val path = cursor.getString(dataColumn) val path = cursor.getString(dataColumn)
if (path.isBlank() || !File(path).exists()) { if (path.isBlank() || !File(path).exists()) {
deleted.add(id) deleted.add(id)
continue continue
}
val mediaType = cursor.getInt(mediaTypeColumn)
val name = cursor.getString(nameColumn)
// Date taken is milliseconds since epoch, Date added is seconds since epoch
val takenAt = cursor.getLong(dateTakenColumn).takeIf { it > 0 } ?: (cursor.getLong(
dateAddedColumn
) * 1000)
val createdAt = formatter.format(Instant.ofEpochMilli(takenAt))
// Date modified is seconds since epoch
val modifiedAt =
formatter.format(Instant.ofEpochMilli(cursor.getLong(dateModifiedColumn) * 1000))
// Duration is milliseconds
val duration =
if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 else cursor.getLong(
durationColumn
) / 1000
val bucketId = cursor.getString(bucketIdColumn)
changed.add(
Asset(
id,
name,
mediaType.toLong(),
createdAt,
modifiedAt,
duration,
listOf(bucketId)
)
)
} }
val mediaType = cursor.getInt(mediaTypeColumn)
val name = cursor.getString(nameColumn)
// Date taken is milliseconds since epoch, Date added is seconds since epoch
val createdAt = (cursor.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
?: cursor.getLong(dateAddedColumn)
// Date modified is seconds since epoch
val modifiedAt = cursor.getLong(dateModifiedColumn) * 1000
// Duration is milliseconds
val duration =
if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 else cursor.getLong(
durationColumn
) / 1000
val bucketId = cursor.getString(bucketIdColumn)
changed.add(
Asset(
id,
name,
mediaType.toLong(),
createdAt,
modifiedAt,
duration,
listOf(bucketId)
)
)
} }
} }
// Unmounted volumes are handled in dart when the album is removed
callback(Result.success(SyncDelta(hasChanges, changed, deleted)))
} catch (e: Exception) {
callback(Result.failure(e))
} }
// Unmounted volumes are handled in dart when the album is removed
return SyncDelta(hasChanges, changed, deleted)
} }
} }

View File

@ -82,8 +82,8 @@ data class Asset (
val id: String, val id: String,
val name: String, val name: String,
val type: Long, val type: Long,
val createdAt: String? = null, val createdAt: Long? = null,
val updatedAt: String? = null, val updatedAt: Long? = null,
val durationInSeconds: Long, val durationInSeconds: Long,
val albumIds: List<String> val albumIds: List<String>
) )
@ -93,8 +93,8 @@ data class Asset (
val id = pigeonVar_list[0] as String val id = pigeonVar_list[0] as String
val name = pigeonVar_list[1] as String val name = pigeonVar_list[1] as String
val type = pigeonVar_list[2] as Long val type = pigeonVar_list[2] as Long
val createdAt = pigeonVar_list[3] as String? val createdAt = pigeonVar_list[3] as Long?
val updatedAt = pigeonVar_list[4] as String? val updatedAt = pigeonVar_list[4] as Long?
val durationInSeconds = pigeonVar_list[5] as Long val durationInSeconds = pigeonVar_list[5] as Long
val albumIds = pigeonVar_list[6] as List<String> val albumIds = pigeonVar_list[6] as List<String>
return Asset(id, name, type, createdAt, updatedAt, durationInSeconds, albumIds) return Asset(id, name, type, createdAt, updatedAt, durationInSeconds, albumIds)
@ -187,13 +187,12 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
} }
} }
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface ImHostService { interface ImHostService {
fun shouldFullSync(callback: (Result<Boolean>) -> Unit) fun shouldFullSync(): Boolean
fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) fun getMediaChanges(): SyncDelta
fun checkpointSync(callback: (Result<Unit>) -> Unit) fun checkpointSync()
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) fun getAssetIdsForAlbum(albumId: String): List<String>
companion object { companion object {
/** The codec used by ImHostService. */ /** The codec used by ImHostService. */
@ -209,15 +208,12 @@ interface ImHostService {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$separatedMessageChannelSuffix", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$separatedMessageChannelSuffix", codec)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
api.shouldFullSync{ result: Result<Boolean> -> val wrapped: List<Any?> = try {
val error = result.exceptionOrNull() listOf(api.shouldFullSync())
if (error != null) { } catch (exception: Throwable) {
reply.reply(MessagesPigeonUtils.wrapError(error)) MessagesPigeonUtils.wrapError(exception)
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@ -227,15 +223,12 @@ interface ImHostService {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
api.getMediaChanges{ result: Result<SyncDelta> -> val wrapped: List<Any?> = try {
val error = result.exceptionOrNull() listOf(api.getMediaChanges())
if (error != null) { } catch (exception: Throwable) {
reply.reply(MessagesPigeonUtils.wrapError(error)) MessagesPigeonUtils.wrapError(exception)
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@ -245,14 +238,13 @@ interface ImHostService {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$separatedMessageChannelSuffix", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$separatedMessageChannelSuffix", codec)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
api.checkpointSync{ result: Result<Unit> -> val wrapped: List<Any?> = try {
val error = result.exceptionOrNull() api.checkpointSync()
if (error != null) { listOf(null)
reply.reply(MessagesPigeonUtils.wrapError(error)) } catch (exception: Throwable) {
} else { MessagesPigeonUtils.wrapError(exception)
reply.reply(MessagesPigeonUtils.wrapResult(null))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@ -264,15 +256,12 @@ interface ImHostService {
channel.setMessageHandler { message, reply -> channel.setMessageHandler { message, reply ->
val args = message as List<Any?> val args = message as List<Any?>
val albumIdArg = args[0] as String val albumIdArg = args[0] as String
api.getAssetIdsForAlbum(albumIdArg) { result: Result<List<String>> -> val wrapped: List<Any?> = try {
val error = result.exceptionOrNull() listOf(api.getAssetIdsForAlbum(albumIdArg))
if (error != null) { } catch (exception: Throwable) {
reply.reply(MessagesPigeonUtils.wrapError(error)) MessagesPigeonUtils.wrapError(exception)
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)

View File

@ -20,38 +20,27 @@ class MessagesImpl(context: Context) : ImHostService {
IllegalStateException("Method not supported on this Android version.") IllegalStateException("Method not supported on this Android version.")
} }
override fun shouldFullSync(callback: (Result<Boolean>) -> Unit) { override fun shouldFullSync(): Boolean =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || mediaManager.shouldFullSync()
mediaManager.shouldFullSync(callback)
} else { override fun getMediaChanges(): SyncDelta {
callback(Result.success(true)) if (!isMediaChangesSupported()) {
throw unsupportedFeatureException()
} }
return mediaManager.getMediaChanges()
} }
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) { override fun checkpointSync() {
if (isMediaChangesSupported()) { if (!isMediaChangesSupported()) {
mediaManager.getMediaChanges(callback) return
} else {
callback(Result.failure(unsupportedFeatureException()))
} }
mediaManager.checkpointSync()
} }
override fun checkpointSync(callback: (Result<Unit>) -> Unit) { override fun getAssetIdsForAlbum(albumId: String): List<String> {
if (isMediaChangesSupported()) { if (!isMediaChangesSupported()) {
mediaManager.checkpointSync(callback) throw unsupportedFeatureException()
} else {
callback(Result.success(Unit))
}
}
override fun getAssetIdsForAlbum(
albumId: String,
callback: (Result<List<String>>) -> Unit
) {
if (isMediaChangesSupported()) {
mediaManager.getAssetIdsForAlbum(albumId, callback)
} else {
callback(Result.failure(unsupportedFeatureException()))
} }
return mediaManager.getAssetIdsForAlbum(albumId)
} }
} }

View File

@ -1,7 +1,7 @@
import Photos import Photos
class WrapperAsset: Hashable, Equatable { struct AssetWrapper: Hashable, Equatable {
var asset: Asset let asset: Asset
init(with asset: Asset) { init(with asset: Asset) {
self.asset = asset self.asset = asset
@ -11,22 +11,22 @@ class WrapperAsset: Hashable, Equatable {
hasher.combine(self.asset.id) hasher.combine(self.asset.id)
} }
static func == (lhs: WrapperAsset, rhs: WrapperAsset) -> Bool { static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool {
return lhs.asset.id == rhs.asset.id return lhs.asset.id == rhs.asset.id
} }
} }
class MediaManager { class MediaManager {
private let _defaults: UserDefaults private let defaults: UserDefaults
private let _changeTokenKey = "immich:changeToken" private let changeTokenKey = "immich:changeToken"
init(with defaults: UserDefaults = .standard) { init(with defaults: UserDefaults = .standard) {
self._defaults = defaults self.defaults = defaults
} }
@available(iOS 16, *) @available(iOS 16, *)
func _getChangeToken() -> PHPersistentChangeToken? { private func getChangeToken() -> PHPersistentChangeToken? {
guard let encodedToken = _defaults.data(forKey: _changeTokenKey) else { guard let encodedToken = defaults.data(forKey: changeTokenKey) else {
print("MediaManager::_getChangeToken: Change token not available in UserDefaults") print("MediaManager::_getChangeToken: Change token not available in UserDefaults")
return nil return nil
} }
@ -40,10 +40,10 @@ class MediaManager {
} }
@available(iOS 16, *) @available(iOS 16, *)
func _saveChangeToken(token: PHPersistentChangeToken) -> Void { private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
do { do {
let encodedToken = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) let encodedToken = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
_defaults.set(encodedToken, forKey: _changeTokenKey) defaults.set(encodedToken, forKey: changeTokenKey)
print("MediaManager::_setChangeToken: Change token saved to UserDefaults") print("MediaManager::_setChangeToken: Change token saved to UserDefaults")
} catch { } catch {
print("MediaManager::_setChangeToken: Failed to persist the token to UserDefaults: \(error)") print("MediaManager::_setChangeToken: Failed to persist the token to UserDefaults: \(error)")
@ -51,109 +51,96 @@ class MediaManager {
} }
@available(iOS 16, *) @available(iOS 16, *)
func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) { func checkpointSync() {
_saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken) saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
completion(.success(()))
} }
@available(iOS 16, *) @available(iOS 16, *)
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) { func shouldFullSync() -> Bool {
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
// When we do not have access to photo library, return true to fallback to old sync // When we do not have access to photo library, return true to fallback to old sync
completion(.success(true)) return true
return
} }
guard let storedToken = _getChangeToken() else { guard let storedToken = getChangeToken() else {
// No token exists, perform the initial full sync // No token exists, perform the initial full sync
print("MediaManager::shouldUseOldSync: No token found. Full sync required") print("MediaManager::shouldUseOldSync: No token found. Full sync required")
completion(.success(true)) return true
return
} }
do { do {
_ = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) _ = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
completion(.success(false)) return false
} catch { } catch {
// fallback to using old sync when we cannot detect changes using the available token // fallback to using old sync when we cannot detect changes using the available token
print("MediaManager::shouldUseOldSync: fetchPersistentChanges failed with error (\(error))") print("MediaManager::shouldUseOldSync: fetchPersistentChanges failed with error (\(error))")
completion(.success(true))
} }
return true
} }
@available(iOS 16, *) @available(iOS 16, *)
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) { func getMediaChanges() throws -> SyncDelta {
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
completion(.failure(PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil))) throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
return
} }
guard let storedToken = _getChangeToken() else { guard let storedToken = getChangeToken() else {
// No token exists, definitely need a full sync // No token exists, definitely need a full sync
print("MediaManager::getMediaChanges: No token found") print("MediaManager::getMediaChanges: No token found")
completion(.failure(PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil))) throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
return
} }
let currentToken = PHPhotoLibrary.shared().currentChangeToken let currentToken = PHPhotoLibrary.shared().currentChangeToken
if storedToken == currentToken { if storedToken == currentToken {
completion(.success(SyncDelta(hasChanges: false, updates: [], deletes: []))) return SyncDelta(hasChanges: false, updates: [], deletes: [])
return
} }
do { do {
let result = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
var updatedArr: Set<WrapperAsset> = []
var deletedArr: Set<String> = []
for changes in result {
let details = try changes.changeDetails(for: PHObjectType.asset)
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
let deleted = details.deletedLocalIdentifiers
let options = PHFetchOptions() var updatedAssets: Set<AssetWrapper> = []
options.includeHiddenAssets = false var deletedAssets: Set<String> = []
for change in changes {
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
let updatedAssets = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options) let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
if (updated.isEmpty) { continue }
updatedAssets.enumerateObjects { (asset, _, _) in
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
for i in 0..<result.count {
let asset = result.object(at: i)
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
let predicate = Asset(id: asset.localIdentifier, name: "", type: 0, createdAt: nil, updatedAt: nil, durationInSeconds: 0, albumIds: [])
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
continue
}
let id = asset.localIdentifier let id = asset.localIdentifier
let name = PHAssetResource.assetResources(for: asset).first?.originalFilename ?? asset.title() let name = PHAssetResource.assetResources(for: asset).first?.originalFilename ?? asset.title()
let type: Int64 = Int64(asset.mediaType.rawValue) let type: Int64 = Int64(asset.mediaType.rawValue)
let createdAt = asset.creationDate.map { dateFormatter.string(from: $0) } let createdAt = asset.creationDate?.timeIntervalSince1970
let updatedAt = asset.modificationDate.map { dateFormatter.string(from: $0) } let updatedAt = asset.modificationDate?.timeIntervalSince1970
let durationInSeconds: Int64 = Int64(asset.duration) let durationInSeconds: Int64 = Int64(asset.duration)
let domainAsset = WrapperAsset(with: Asset( let domainAsset = AssetWrapper(with: Asset(
id: id, id: id,
name: name, name: name,
type: type, type: type,
createdAt: createdAt, createdAt: createdAt.map { Int64($0) },
updatedAt: updatedAt, updatedAt: updatedAt.map { Int64($0) },
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
albumIds: self._getAlbumIds(forAsset: asset) albumIds: self._getAlbumIds(forAsset: asset)
)) ))
updatedArr.insert(domainAsset) updatedAssets.insert(domainAsset)
} }
deletedArr.formUnion(deleted) deletedAssets.formUnion(details.deletedLocalIdentifiers)
} }
let delta = SyncDelta(hasChanges: true, updates: Array(updatedArr.map { $0.asset }), deletes: Array(deletedArr)) return SyncDelta(hasChanges: true, updates: Array(updatedAssets.map { $0.asset }), deletes: Array(deletedAssets))
completion(.success(delta))
return
} catch {
print("MediaManager::getMediaChanges: Error fetching persistent changes: \(error)")
completion(.failure(PigeonError(code: "3", message: error.localizedDescription, details: nil)))
return
} }
} }

View File

@ -133,8 +133,8 @@ struct Asset: Hashable {
var id: String var id: String
var name: String var name: String
var type: Int64 var type: Int64
var createdAt: String? = nil var createdAt: Int64? = nil
var updatedAt: String? = nil var updatedAt: Int64? = nil
var durationInSeconds: Int64 var durationInSeconds: Int64
var albumIds: [String] var albumIds: [String]
@ -144,8 +144,8 @@ struct Asset: Hashable {
let id = pigeonVar_list[0] as! String let id = pigeonVar_list[0] as! String
let name = pigeonVar_list[1] as! String let name = pigeonVar_list[1] as! String
let type = pigeonVar_list[2] as! Int64 let type = pigeonVar_list[2] as! Int64
let createdAt: String? = nilOrValue(pigeonVar_list[3]) let createdAt: Int64? = nilOrValue(pigeonVar_list[3])
let updatedAt: String? = nilOrValue(pigeonVar_list[4]) let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
let durationInSeconds = pigeonVar_list[5] as! Int64 let durationInSeconds = pigeonVar_list[5] as! Int64
let albumIds = pigeonVar_list[6] as! [String] let albumIds = pigeonVar_list[6] as! [String]
@ -251,13 +251,12 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
} }
/// Generated protocol from Pigeon that represents a handler of messages from Flutter. /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol ImHostService { protocol ImHostService {
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) func shouldFullSync() throws -> Bool
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) func getMediaChanges() throws -> SyncDelta
func checkpointSync(completion: @escaping (Result<Void, Error>) -> Void) func checkpointSync() throws
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void) func getAssetIdsForAlbum(albumId: String) throws -> [String]
} }
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@ -274,13 +273,11 @@ class ImHostServiceSetup {
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api { if let api = api {
shouldFullSyncChannel.setMessageHandler { _, reply in shouldFullSyncChannel.setMessageHandler { _, reply in
api.shouldFullSync { result in do {
switch result { let result = try api.shouldFullSync()
case .success(let res): reply(wrapResult(result))
reply(wrapResult(res)) } catch {
case .failure(let error): reply(wrapError(error))
reply(wrapError(error))
}
} }
} }
} else { } else {
@ -291,13 +288,11 @@ class ImHostServiceSetup {
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api { if let api = api {
getMediaChangesChannel.setMessageHandler { _, reply in getMediaChangesChannel.setMessageHandler { _, reply in
api.getMediaChanges { result in do {
switch result { let result = try api.getMediaChanges()
case .success(let res): reply(wrapResult(result))
reply(wrapResult(res)) } catch {
case .failure(let error): reply(wrapError(error))
reply(wrapError(error))
}
} }
} }
} else { } else {
@ -306,13 +301,11 @@ class ImHostServiceSetup {
let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api { if let api = api {
checkpointSyncChannel.setMessageHandler { _, reply in checkpointSyncChannel.setMessageHandler { _, reply in
api.checkpointSync { result in do {
switch result { try api.checkpointSync()
case .success: reply(wrapResult(nil))
reply(wrapResult(nil)) } catch {
case .failure(let error): reply(wrapError(error))
reply(wrapError(error))
}
} }
} }
} else { } else {
@ -325,13 +318,11 @@ class ImHostServiceSetup {
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
let args = message as! [Any?] let args = message as! [Any?]
let albumIdArg = args[0] as! String let albumIdArg = args[0] as! String
api.getAssetIdsForAlbum(albumId: albumIdArg) { result in do {
switch result { let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
case .success(let res): reply(wrapResult(result))
reply(wrapResult(res)) } catch {
case .failure(let error): reply(wrapError(error))
reply(wrapError(error))
}
} }
} }
} else { } else {

View File

@ -1,39 +1,36 @@
import Photos import Photos
class ImHostServiceImpl: ImHostService { class ImHostServiceImpl: ImHostService {
let _mediaManager: MediaManager private let mediaManager: MediaManager
init() { init() {
self._mediaManager = MediaManager() self.mediaManager = MediaManager()
} }
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) { func shouldFullSync() throws -> Bool {
if #available(iOS 16, *) { return if #available(iOS 16, *) {
_mediaManager.shouldFullSync(completion: completion) mediaManager.shouldFullSync()
} else { } else {
// Always fall back to full sync on older iOS versions // Always fall back to full sync on older iOS versions
completion(.success(true)) true
} }
} }
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) { func getMediaChanges() throws -> SyncDelta {
guard #available(iOS 16, *) else { guard #available(iOS 16, *) else {
completion(.failure(PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil))) throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
return
} }
_mediaManager.getMediaChanges(completion: completion) return try mediaManager.getMediaChanges()
} }
func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) { func checkpointSync() throws {
if #available(iOS 16, *) { if #available(iOS 16, *) {
_mediaManager.checkpointSync(completion: completion) mediaManager.checkpointSync()
} else {
completion(.success(()))
} }
} }
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], any Error>) -> Void) { func getAssetIdsForAlbum(albumId: String) throws -> [String] {
// Android specific, empty list is safe no-op // Android specific, empty list is safe no-op
completion(.success([])) return []
} }
} }

View File

@ -354,10 +354,12 @@ extension on platform.Asset {
id: id, id: id,
name: name, name: name,
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other, type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
createdAt: createdAt: createdAt == null
createdAt == null ? DateTime.now() : DateTime.parse(createdAt!), ? DateTime.now()
updatedAt: : DateTime.fromMillisecondsSinceEpoch(createdAt! * 1000),
updatedAt == null ? DateTime.now() : DateTime.parse(updatedAt!), updatedAt: updatedAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(updatedAt! * 1000),
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
); );
} }

View File

@ -16,8 +16,9 @@ class Asset {
final String id; final String id;
final String name; final String name;
final int type; // follows AssetType enum from base_asset.model.dart final int type; // follows AssetType enum from base_asset.model.dart
final String? createdAt; // Seconds since epoch
final String? updatedAt; final int? createdAt;
final int? updatedAt;
final int durationInSeconds; final int durationInSeconds;
final List<String> albumIds; final List<String> albumIds;
@ -25,10 +26,10 @@ class Asset {
required this.id, required this.id,
required this.name, required this.name,
required this.type, required this.type,
required this.createdAt, this.createdAt,
required this.updatedAt, this.updatedAt,
required this.durationInSeconds, this.durationInSeconds = 0,
required this.albumIds, this.albumIds = const [],
}); });
} }
@ -45,17 +46,13 @@ class SyncDelta {
@HostApi() @HostApi()
abstract class ImHostService { abstract class ImHostService {
@async
bool shouldFullSync(); bool shouldFullSync();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
SyncDelta getMediaChanges(); SyncDelta getMediaChanges();
@async
void checkpointSync(); void checkpointSync();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<String> getAssetIdsForAlbum(String albumId); List<String> getAssetIdsForAlbum(String albumId);
} }

View File

@ -14,22 +14,21 @@ PlatformException _createConnectionError(String channelName) {
message: 'Unable to establish connection on channel: "$channelName".', message: 'Unable to establish connection on channel: "$channelName".',
); );
} }
bool _deepEquals(Object? a, Object? b) { bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) { if (a is List && b is List) {
return a.length == b.length && return a.length == b.length &&
a.indexed a.indexed
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
} }
if (a is Map && b is Map) { if (a is Map && b is Map) {
return a.length == b.length && return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
a.entries.every((MapEntry<Object?, Object?> entry) => (b as Map<Object?, Object?>).containsKey(entry.key) &&
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]));
_deepEquals(entry.value, b[entry.key]));
} }
return a == b; return a == b;
} }
class Asset { class Asset {
Asset({ Asset({
required this.id, required this.id,
@ -47,9 +46,9 @@ class Asset {
int type; int type;
String? createdAt; int? createdAt;
String? updatedAt; int? updatedAt;
int durationInSeconds; int durationInSeconds;
@ -68,8 +67,7 @@ class Asset {
} }
Object encode() { Object encode() {
return _toList(); return _toList(); }
}
static Asset decode(Object result) { static Asset decode(Object result) {
result as List<Object?>; result as List<Object?>;
@ -77,8 +75,8 @@ class Asset {
id: result[0]! as String, id: result[0]! as String,
name: result[1]! as String, name: result[1]! as String,
type: result[2]! as int, type: result[2]! as int,
createdAt: result[3] as String?, createdAt: result[3] as int?,
updatedAt: result[4] as String?, updatedAt: result[4] as int?,
durationInSeconds: result[5]! as int, durationInSeconds: result[5]! as int,
albumIds: (result[6] as List<Object?>?)!.cast<String>(), albumIds: (result[6] as List<Object?>?)!.cast<String>(),
); );
@ -98,7 +96,8 @@ class Asset {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList()); int get hashCode => Object.hashAll(_toList())
;
} }
class SyncDelta { class SyncDelta {
@ -123,8 +122,7 @@ class SyncDelta {
} }
Object encode() { Object encode() {
return _toList(); return _toList(); }
}
static SyncDelta decode(Object result) { static SyncDelta decode(Object result) {
result as List<Object?>; result as List<Object?>;
@ -149,9 +147,11 @@ class SyncDelta {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList()); int get hashCode => Object.hashAll(_toList())
;
} }
class _PigeonCodec extends StandardMessageCodec { class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec(); const _PigeonCodec();
@override @override
@ -159,10 +159,10 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is Asset) { } else if (value is Asset) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is SyncDelta) { } else if (value is SyncDelta) {
buffer.putUint8(130); buffer.putUint8(130);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else { } else {
@ -173,9 +173,9 @@ class _PigeonCodec extends StandardMessageCodec {
@override @override
Object? readValueOfType(int type, ReadBuffer buffer) { Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) { switch (type) {
case 129: case 129:
return Asset.decode(readValue(buffer)!); return Asset.decode(readValue(buffer)!);
case 130: case 130:
return SyncDelta.decode(readValue(buffer)!); return SyncDelta.decode(readValue(buffer)!);
default: default:
return super.readValueOfType(type, buffer); return super.readValueOfType(type, buffer);
@ -187,11 +187,9 @@ class ImHostService {
/// Constructor for [ImHostService]. The [binaryMessenger] named argument is /// Constructor for [ImHostService]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default /// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform. /// BinaryMessenger will be used which routes to the host platform.
ImHostService( ImHostService({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
{BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger, : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger; final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@ -199,10 +197,8 @@ class ImHostService {
final String pigeonVar_messageChannelSuffix; final String pigeonVar_messageChannelSuffix;
Future<bool> shouldFullSync() async { Future<bool> shouldFullSync() async {
final String pigeonVar_channelName = final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$pigeonVar_messageChannelSuffix'; final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
@ -229,10 +225,8 @@ class ImHostService {
} }
Future<SyncDelta> getMediaChanges() async { Future<SyncDelta> getMediaChanges() async {
final String pigeonVar_channelName = final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$pigeonVar_messageChannelSuffix'; final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
@ -259,10 +253,8 @@ class ImHostService {
} }
Future<void> checkpointSync() async { Future<void> checkpointSync() async {
final String pigeonVar_channelName = final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$pigeonVar_messageChannelSuffix'; final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
@ -284,16 +276,13 @@ class ImHostService {
} }
Future<List<String>> getAssetIdsForAlbum(String albumId) async { Future<List<String>> getAssetIdsForAlbum(String albumId) async {
final String pigeonVar_channelName = final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId]);
pigeonVar_channel.send(<Object?>[albumId]);
final List<Object?>? pigeonVar_replyList = final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?; await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {