Compare commits

...

4 Commits

Author SHA1 Message Date
shenlong-tanwen
f632e4f666 review changes 2025-09-29 05:46:09 +05:30
shenlong-tanwen
0e8492ceba trailing closures in swift 2025-09-29 03:38:29 +05:30
shenlong-tanwen
13abe14142 async ios 2025-09-29 00:10:34 +05:30
shenlong-tanwen
ae595f2947 fix: android - execute in background 2025-09-29 00:10:34 +05:30
27 changed files with 350 additions and 196 deletions

View File

@@ -3,7 +3,6 @@ package app.alextran.immich
import android.app.Application
import androidx.work.Configuration
import androidx.work.WorkManager
import app.alextran.immich.background.BackgroundWorkerApiImpl
class ImmichApp : Application() {
override fun onCreate() {
@@ -17,6 +16,5 @@ class ImmichApp : Application() {
// As a workaround, we also run a backup check when initializing the application
ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0)
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
}
}

View File

@@ -0,0 +1,16 @@
package app.alextran.immich
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun <T> dispatch(
dispatcher: CoroutineDispatcher = Dispatchers.IO,
callback: (Result<T>) -> Unit,
block: () -> T
) {
CoroutineScope(dispatcher).launch {
callback(runCatching { block() })
}
}

View File

@@ -2,49 +2,50 @@ package app.alextran.immich.background
import android.content.Context
import android.util.Log
import app.alextran.immich.dispatch
import io.flutter.embedding.engine.plugins.FlutterPlugin
import java.util.concurrent.atomic.AtomicInteger
private const val TAG = "BackgroundEngineLock"
class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, FlutterPlugin {
private val ctx: Context = context.applicationContext
private val ctx: Context = context.applicationContext
companion object {
companion object {
private var engineCount = AtomicInteger(0)
private var engineCount = AtomicInteger(0)
private fun checkAndEnforceBackgroundLock(ctx: Context) {
// work manager task is running while the main app is opened, cancel the worker
if (BackgroundWorkerPreferences(ctx).isLocked() &&
engineCount.get() > 1 &&
BackgroundWorkerApiImpl.isBackgroundWorkerRunning()
) {
Log.i(TAG, "Background worker is locked, cancelling the background worker")
BackgroundWorkerApiImpl.cancelBackgroundWorker(ctx)
}
}
private fun checkAndEnforceBackgroundLock(ctx: Context) {
// work manager task is running while the main app is opened, cancel the worker
if (BackgroundWorkerPreferences(ctx).isLocked() &&
engineCount.get() > 1 &&
BackgroundWorkerApiImpl.isBackgroundWorkerRunning()
) {
Log.i(TAG, "Background worker is locked, cancelling the background worker")
BackgroundWorkerApiImpl.cancelBackgroundWorker(ctx)
}
}
}
override fun lock() {
BackgroundWorkerPreferences(ctx).setLocked(true)
checkAndEnforceBackgroundLock(ctx)
Log.i(TAG, "Background worker is locked")
}
override fun lock(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
BackgroundWorkerPreferences(ctx).setLocked(true)
checkAndEnforceBackgroundLock(ctx)
Log.i(TAG, "Background worker is locked")
}
override fun unlock() {
BackgroundWorkerPreferences(ctx).setLocked(false)
Log.i(TAG, "Background worker is unlocked")
}
override fun unlock(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
BackgroundWorkerPreferences(ctx).setLocked(false)
Log.i(TAG, "Background worker is unlocked")
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
checkAndEnforceBackgroundLock(binding.applicationContext)
engineCount.incrementAndGet()
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
checkAndEnforceBackgroundLock(binding.applicationContext)
engineCount.incrementAndGet()
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
engineCount.decrementAndGet()
Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount")
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
engineCount.decrementAndGet()
Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount")
}
}

View File

@@ -133,11 +133,12 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerFgHostApi {
fun enable()
fun configure(settings: BackgroundWorkerSettings)
fun disable()
fun enable(callback: (Result<Unit>) -> Unit)
fun configure(settings: BackgroundWorkerSettings, callback: (Result<Unit>) -> Unit)
fun disable(callback: (Result<Unit>) -> Unit)
companion object {
/** The codec used by BackgroundWorkerFgHostApi. */
@@ -152,13 +153,14 @@ interface BackgroundWorkerFgHostApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.enable()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
api.enable{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
} else {
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
@@ -170,13 +172,14 @@ interface BackgroundWorkerFgHostApi {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val settingsArg = args[0] as BackgroundWorkerSettings
val wrapped: List<Any?> = try {
api.configure(settingsArg)
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
api.configure(settingsArg) { result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
} else {
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
@@ -186,13 +189,14 @@ interface BackgroundWorkerFgHostApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.disable()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
api.disable{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
} else {
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)

View File

@@ -215,7 +215,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
if (foregroundFuture != null && !foregroundFuture.isCancelled && !foregroundFuture.isDone) {
try {
foregroundFuture.get(500, TimeUnit.MILLISECONDS)
} catch (e: Exception) {
} catch (_: Exception) {
// ignored, there is nothing to be done
}
}

View File

@@ -8,6 +8,7 @@ import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import app.alextran.immich.dispatch
import io.flutter.embedding.engine.FlutterEngineCache
import java.util.concurrent.TimeUnit
@@ -16,16 +17,18 @@ private const val TAG = "BackgroundWorkerApiImpl"
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
private val ctx: Context = context.applicationContext
override fun enable() {
enqueueMediaObserver(ctx)
}
override fun enable(callback: (Result<Unit>) -> Unit) =
dispatch(callback = callback) { enqueueMediaObserver(ctx) }
override fun configure(settings: BackgroundWorkerSettings) {
override fun configure(
settings: BackgroundWorkerSettings,
callback: (Result<Unit>) -> Unit
) = dispatch(callback = callback) {
BackgroundWorkerPreferences(ctx).updateSettings(settings)
enqueueMediaObserver(ctx)
}
override fun disable() {
override fun disable(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
WorkManager.getInstance(ctx).apply {
cancelUniqueWork(OBSERVER_WORKER_NAME)
cancelUniqueWork(BACKGROUND_WORKER_NAME)
@@ -38,7 +41,6 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
fun enqueueMediaObserver(ctx: Context) {
val settings = BackgroundWorkerPreferences(ctx).getSettings()
val constraints = Constraints.Builder().apply {

View File

@@ -44,10 +44,11 @@ private open class BackgroundWorkerLockPigeonCodec : StandardMessageCodec() {
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerLockApi {
fun lock()
fun unlock()
fun lock(callback: (Result<Unit>) -> Unit)
fun unlock(callback: (Result<Unit>) -> Unit)
companion object {
/** The codec used by BackgroundWorkerLockApi. */
@@ -62,13 +63,14 @@ interface BackgroundWorkerLockApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.lock()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerLockPigeonUtils.wrapError(exception)
api.lock{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(BackgroundWorkerLockPigeonUtils.wrapError(error))
} else {
reply.reply(BackgroundWorkerLockPigeonUtils.wrapResult(null))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
@@ -78,13 +80,14 @@ interface BackgroundWorkerLockApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.unlock()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerLockPigeonUtils.wrapError(exception)
api.unlock{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(BackgroundWorkerLockPigeonUtils.wrapError(error))
} else {
reply.reply(BackgroundWorkerLockPigeonUtils.wrapResult(null))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)

View File

@@ -82,9 +82,10 @@ private open class ConnectivityPigeonCodec : StandardMessageCodec() {
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface ConnectivityApi {
fun getCapabilities(): List<NetworkCapability>
fun getCapabilities(callback: (Result<List<NetworkCapability>>) -> Unit)
companion object {
/** The codec used by ConnectivityApi. */
@@ -100,12 +101,15 @@ interface ConnectivityApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getCapabilities())
} catch (exception: Throwable) {
ConnectivityPigeonUtils.wrapError(exception)
api.getCapabilities{ result: Result<List<NetworkCapability>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(ConnectivityPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(ConnectivityPigeonUtils.wrapResult(data))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiManager
import app.alextran.immich.dispatch
class ConnectivityApiImpl(context: Context) : ConnectivityApi {
private val connectivityManager =
@@ -11,7 +12,13 @@ class ConnectivityApiImpl(context: Context) : ConnectivityApi {
private val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override fun getCapabilities(): List<NetworkCapability> {
override fun getCapabilities(callback: (Result<List<NetworkCapability>>) -> Unit) =
dispatch(callback = callback) {
getCapabilities()
}
private fun getCapabilities(): List<NetworkCapability> {
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?: return emptyList()

View File

@@ -296,13 +296,13 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface NativeSyncApi {
fun shouldFullSync(): Boolean
fun getMediaChanges(): SyncDelta
fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit)
fun checkpointSync()
fun clearSyncCheckpoint()
fun getAssetIdsForAlbum(albumId: String): List<String>
fun getAlbums(): List<PlatformAlbum>
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit)
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit)
fun getAssetsCountSince(albumId: String, timestamp: Long, callback: (Result<Long>) -> Unit)
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit)
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
fun cancelHashing()
@@ -335,12 +335,15 @@ interface NativeSyncApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getMediaChanges())
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
api.getMediaChanges{ result: Result<SyncDelta> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
@@ -384,12 +387,15 @@ interface NativeSyncApi {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val albumIdArg = args[0] as String
val wrapped: List<Any?> = try {
listOf(api.getAssetIdsForAlbum(albumIdArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
api.getAssetIdsForAlbum(albumIdArg) { result: Result<List<String>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
@@ -399,12 +405,15 @@ interface NativeSyncApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getAlbums())
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
api.getAlbums{ result: Result<List<PlatformAlbum>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
@@ -417,12 +426,15 @@ interface NativeSyncApi {
val args = message as List<Any?>
val albumIdArg = args[0] as String
val timestampArg = args[1] as Long
val wrapped: List<Any?> = try {
listOf(api.getAssetsCountSince(albumIdArg, timestampArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
api.getAssetsCountSince(albumIdArg, timestampArg) { result: Result<Long> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
@@ -435,12 +447,15 @@ interface NativeSyncApi {
val args = message as List<Any?>
val albumIdArg = args[0] as String
val updatedTimeCondArg = args[1] as Long?
val wrapped: List<Any?> = try {
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg) { result: Result<List<PlatformAsset>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)

View File

@@ -18,7 +18,7 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
// No-op for Android 10 and below
}
override fun getMediaChanges(): SyncDelta {
throw IllegalStateException("Method not supported on this Android version.")
}
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) =
callback(Result.failure(IllegalStateException("Method not supported on this Android version.")))
}

View File

@@ -5,6 +5,7 @@ import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension
import app.alextran.immich.dispatch
import kotlinx.serialization.json.Json
@RequiresApi(Build.VERSION_CODES.Q)
@@ -47,7 +48,12 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
}
}
override fun getMediaChanges(): SyncDelta {
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) =
dispatch(callback = callback) {
getMediaChanges()
}
private fun getMediaChanges(): SyncDelta {
val genMap = getSavedGenerationMap()
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
val changed = mutableListOf<PlatformAsset>()

View File

@@ -7,6 +7,7 @@ import android.database.Cursor
import android.provider.MediaStore
import android.util.Base64
import androidx.core.database.getStringOrNull
import app.alextran.immich.dispatch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -145,7 +146,10 @@ open class NativeSyncApiImplBase(context: Context) {
}
}
fun getAlbums(): List<PlatformAlbum> {
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit) =
dispatch(callback = callback) { getAlbums() }
private fun getAlbums(): List<PlatformAlbum> {
val albums = mutableListOf<PlatformAlbum>()
val albumsCount = mutableMapOf<String, Int>()
@@ -192,7 +196,10 @@ open class NativeSyncApiImplBase(context: Context) {
.sortedBy { it.id }
}
fun getAssetIdsForAlbum(albumId: String): List<String> {
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) =
dispatch(callback = callback) { getAssetIdsForAlbum(albumId); }
private fun getAssetIdsForAlbum(albumId: String): List<String> {
val projection = arrayOf(MediaStore.MediaColumns._ID)
return getCursor(
@@ -208,15 +215,23 @@ open class NativeSyncApiImplBase(context: Context) {
} ?: emptyList()
}
fun getAssetsCountSince(albumId: String, timestamp: Long): Long =
fun getAssetsCountSince(albumId: String, timestamp: Long, callback: (Result<Long>) -> Unit) =
dispatch(callback = callback) { getAssetsCountSince(albumId, timestamp) }
private fun getAssetsCountSince(albumId: String, timestamp: Long): Long =
getCursor(
MediaStore.VOLUME_EXTERNAL,
"$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION",
arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS),
)?.use { cursor -> cursor.count.toLong() } ?: 0L
fun getAssetsForAlbum(
albumId: String,
updatedTimeCond: Long?,
callback: (Result<List<PlatformAsset>>) -> Unit
) = dispatch(callback = callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
private fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
@@ -254,7 +269,7 @@ open class NativeSyncApiImplBase(context: Context) {
}.awaitAll()
callback(Result.success(results))
} catch (e: CancellationException) {
} catch (_: CancellationException) {
callback(
Result.failure(
FlutterError(

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
@@ -133,11 +133,14 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
B2D27ABE2E84A0FF004DD55B /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Core;
sourceTree = "<group>";
};
F0B57D3D2DF764BD00DC5BCC /* WidgetExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
@@ -247,6 +250,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
B2D27ABE2E84A0FF004DD55B /* Core */,
B25D37792E72CA15008B6CA7 /* Connectivity */,
B21E34A62E5AF9760031FDB9 /* Background */,
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
@@ -332,6 +336,7 @@
);
fileSystemSynchronizedGroups = (
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
B2D27ABE2E84A0FF004DD55B /* Core */,
);
name = Runner;
productName = Runner;
@@ -521,10 +526,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -553,10 +562,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";

View File

@@ -179,11 +179,12 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
static let shared = BackgroundWorkerPigeonCodec(readerWriter: BackgroundWorkerPigeonCodecReaderWriter())
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol BackgroundWorkerFgHostApi {
func enable() throws
func configure(settings: BackgroundWorkerSettings) throws
func disable() throws
func enable(completion: @escaping (Result<Void, Error>) -> Void)
func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result<Void, Error>) -> Void)
func disable(completion: @escaping (Result<Void, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -195,11 +196,13 @@ class BackgroundWorkerFgHostApiSetup {
let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
enableChannel.setMessageHandler { _, reply in
do {
try api.enable()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
api.enable { result in
switch result {
case .success:
reply(wrapResult(nil))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
@@ -210,11 +213,13 @@ class BackgroundWorkerFgHostApiSetup {
configureChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let settingsArg = args[0] as! BackgroundWorkerSettings
do {
try api.configure(settings: settingsArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
api.configure(settings: settingsArg) { result in
switch result {
case .success:
reply(wrapResult(nil))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
@@ -223,11 +228,13 @@ class BackgroundWorkerFgHostApiSetup {
let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
disableChannel.setMessageHandler { _, reply in
do {
try api.disable()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
api.disable { result in
switch result {
case .success:
reply(wrapResult(nil))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {

View File

@@ -1,23 +1,27 @@
import BackgroundTasks
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
func enable() throws {
BackgroundWorkerApiImpl.scheduleRefreshWorker()
BackgroundWorkerApiImpl.scheduleProcessingWorker()
print("BackgroundWorkerApiImpl:enable Background worker scheduled")
func enable(completion: @escaping (Result<Void, any Error>) -> Void) {
dispatch(completion: completion) {
BackgroundWorkerApiImpl.scheduleRefreshWorker()
BackgroundWorkerApiImpl.scheduleProcessingWorker()
print("BackgroundWorkerApiImpl:enable Background worker scheduled")
}
}
func configure(settings: BackgroundWorkerSettings) throws {
func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result<Void, any Error>) -> Void) {
// Android only
completion(Result.success(Void()))
}
func disable() throws {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
func disable(completion: @escaping (Result<Void, any Error>) -> Void) {
dispatch(completion: completion) {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
}
}
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
private static let taskSemaphore = DispatchSemaphore(value: 1)

View File

@@ -94,9 +94,10 @@ class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter())
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol ConnectivityApi {
func getCapabilities() throws -> [NetworkCapability]
func getCapabilities(completion: @escaping (Result<[NetworkCapability], Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -115,11 +116,13 @@ class ConnectivityApiSetup {
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getCapabilitiesChannel.setMessageHandler { _, reply in
do {
let result = try api.getCapabilities()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
api.getCapabilities { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {

View File

@@ -1,6 +1,6 @@
class ConnectivityApiImpl: ConnectivityApi {
func getCapabilities() throws -> [NetworkCapability] {
[]
func getCapabilities(completion: @escaping (Result<[NetworkCapability], any Error>) -> Void) {
completion(Result.success([]))
}
}

View File

@@ -0,0 +1,9 @@
func dispatch<T>(
qos: DispatchQoS.QoSClass = .default,
completion: @escaping (Result<T, Error>) -> Void,
block: @escaping () throws -> T
) {
DispatchQueue.global(qos: qos).async {
completion(Result { try block() })
}
}

View File

@@ -355,13 +355,13 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol NativeSyncApi {
func shouldFullSync() throws -> Bool
func getMediaChanges() throws -> SyncDelta
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void)
func checkpointSync() throws
func clearSyncCheckpoint() throws
func getAssetIdsForAlbum(albumId: String) throws -> [String]
func getAlbums() throws -> [PlatformAlbum]
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void)
func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void)
func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result<Int64, Error>) -> Void)
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void)
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
func cancelHashing() throws
}
@@ -395,11 +395,13 @@ class NativeSyncApiSetup {
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getMediaChangesChannel.setMessageHandler { _, reply in
do {
let result = try api.getMediaChanges()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
api.getMediaChanges { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
@@ -438,11 +440,13 @@ class NativeSyncApiSetup {
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let albumIdArg = args[0] as! String
do {
let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
api.getAssetIdsForAlbum(albumId: albumIdArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
@@ -453,11 +457,13 @@ class NativeSyncApiSetup {
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getAlbumsChannel.setMessageHandler { _, reply in
do {
let result = try api.getAlbums()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
api.getAlbums { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
@@ -471,11 +477,13 @@ class NativeSyncApiSetup {
let args = message as! [Any?]
let albumIdArg = args[0] as! String
let timestampArg = args[1] as! Int64
do {
let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
@@ -489,11 +497,13 @@ class NativeSyncApiSetup {
let args = message as! [Any?]
let albumIdArg = args[0] as! String
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
do {
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {

View File

@@ -18,6 +18,7 @@ struct AssetWrapper: Hashable, Equatable {
}
class NativeSyncApiImpl: NativeSyncApi {
private let defaults: UserDefaults
private let changeTokenKey = "immich:changeToken"
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
@@ -75,7 +76,12 @@ class NativeSyncApiImpl: NativeSyncApi {
return false
}
func getAlbums() throws -> [PlatformAlbum] {
func getAlbums(completion: @escaping (Result<[PlatformAlbum], any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion, block: getAlbums)
}
private func getAlbums() throws -> [PlatformAlbum] {
var albums: [PlatformAlbum] = []
albumTypes.forEach { type in
@@ -112,7 +118,11 @@ class NativeSyncApiImpl: NativeSyncApi {
return albums.sorted { $0.id < $1.id }
}
func getMediaChanges() throws -> SyncDelta {
func getMediaChanges(completion: @escaping (Result<SyncDelta, any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion, block: getMediaChanges)
}
private func getMediaChanges() throws -> SyncDelta {
guard #available(iOS 16, *) else {
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
}
@@ -198,7 +208,11 @@ class NativeSyncApiImpl: NativeSyncApi {
return albumAssets
}
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion) { try self.getAssetIdsForAlbum(albumId: albumId) }
}
private func getAssetIdsForAlbum(albumId: String) throws -> [String] {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return []
@@ -214,7 +228,13 @@ class NativeSyncApiImpl: NativeSyncApi {
return ids
}
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result<Int64, any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion) {
try self.getAssetsCountSince(albumId: albumId, timestamp: timestamp)
}
}
private func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return 0
@@ -228,7 +248,13 @@ class NativeSyncApiImpl: NativeSyncApi {
return Int64(assets.count)
}
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion) {
try self.getAssetsForAlbum(albumId: albumId, updatedTimeCond: updatedTimeCond)
}
}
private func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return []

View File

@@ -20,10 +20,13 @@ class BackgroundWorkerSettings {
@HostApi()
abstract class BackgroundWorkerFgHostApi {
@async
void enable();
@async
void configure(BackgroundWorkerSettings settings);
@async
void disable();
}

View File

@@ -11,7 +11,9 @@ import 'package:pigeon/pigeon.dart';
)
@HostApi()
abstract class BackgroundWorkerLockApi {
@async
void lock();
@async
void unlock();
}

View File

@@ -15,6 +15,7 @@ enum NetworkCapability { cellular, wifi, vpn, unmetered }
@HostApi()
abstract class ConnectivityApi {
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<NetworkCapability> getCapabilities();
}

View File

@@ -83,6 +83,7 @@ class HashResult {
abstract class NativeSyncApi {
bool shouldFullSync();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
SyncDelta getMediaChanges();
@@ -90,15 +91,19 @@ abstract class NativeSyncApi {
void clearSyncCheckpoint();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<String> getAssetIdsForAlbum(String albumId);
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<PlatformAlbum> getAlbums();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
int getAssetsCountSince(String albumId, int timestamp);
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<PlatformAsset> getAssetsForAlbum(String albumId, {int? updatedTimeCond});