mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix: android background backups (#21795)
* upload using dart client * add connectivity api * respect backup network setting * comment as to why we need to wait for setForegroundAsync call * log assets skipped due to network constraint * dynamic spawning -> false --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
39c1ebf698
commit
722a464e23
22 changed files with 755 additions and 27 deletions
|
|
@ -5,6 +5,8 @@ import android.os.Build
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
import app.alextran.immich.background.BackgroundWorkerApiImpl
|
import app.alextran.immich.background.BackgroundWorkerApiImpl
|
||||||
import app.alextran.immich.background.BackgroundWorkerFgHostApi
|
import app.alextran.immich.background.BackgroundWorkerFgHostApi
|
||||||
|
import app.alextran.immich.connectivity.ConnectivityApi
|
||||||
|
import app.alextran.immich.connectivity.ConnectivityApiImpl
|
||||||
import app.alextran.immich.images.ThumbnailApi
|
import app.alextran.immich.images.ThumbnailApi
|
||||||
import app.alextran.immich.images.ThumbnailsImpl
|
import app.alextran.immich.images.ThumbnailsImpl
|
||||||
import app.alextran.immich.sync.NativeSyncApi
|
import app.alextran.immich.sync.NativeSyncApi
|
||||||
|
|
@ -34,6 +36,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||||
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
||||||
ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx))
|
ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx))
|
||||||
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
|
||||||
|
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ interface BackgroundWorkerFgHostApi {
|
||||||
/** 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 BackgroundWorkerBgHostApi {
|
interface BackgroundWorkerBgHostApi {
|
||||||
fun onInitialized()
|
fun onInitialized()
|
||||||
|
fun showNotification(title: String, content: String)
|
||||||
fun close()
|
fun close()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -138,6 +139,25 @@ interface BackgroundWorkerBgHostApi {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val titleArg = args[0] as String
|
||||||
|
val contentArg = args[1] as String
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.showNotification(titleArg, contentArg)
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,27 @@
|
||||||
package app.alextran.immich.background
|
package app.alextran.immich.background
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import app.alextran.immich.MainActivity
|
import app.alextran.immich.MainActivity
|
||||||
|
import app.alextran.immich.R
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import io.flutter.FlutterInjector
|
import io.flutter.FlutterInjector
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
import io.flutter.embedding.engine.dart.DartExecutor
|
||||||
import io.flutter.embedding.engine.loader.FlutterLoader
|
import io.flutter.embedding.engine.loader.FlutterLoader
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private const val TAG = "BackgroundWorker"
|
private const val TAG = "BackgroundWorker"
|
||||||
|
|
||||||
|
|
@ -40,15 +49,32 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
/// Flag to track whether the background task has completed to prevent duplicate completions
|
/// Flag to track whether the background task has completed to prevent duplicate completions
|
||||||
private var isComplete = false
|
private var isComplete = false
|
||||||
|
|
||||||
|
private val notificationManager =
|
||||||
|
ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
private var foregroundFuture: ListenableFuture<Void>? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (!loader.initialized()) {
|
if (!loader.initialized()) {
|
||||||
loader.startInitialization(ctx)
|
loader.startInitialization(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_CHANNEL_ID = "immich::background_worker::notif"
|
||||||
|
private const val NOTIFICATION_ID = 100
|
||||||
|
}
|
||||||
|
|
||||||
override fun startWork(): ListenableFuture<Result> {
|
override fun startWork(): ListenableFuture<Result> {
|
||||||
Log.i(TAG, "Starting background upload worker")
|
Log.i(TAG, "Starting background upload worker")
|
||||||
|
|
||||||
|
val notificationChannel = NotificationChannel(
|
||||||
|
NOTIFICATION_CHANNEL_ID,
|
||||||
|
NOTIFICATION_CHANNEL_ID,
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
notificationManager.createNotificationChannel(notificationChannel)
|
||||||
|
|
||||||
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
engine = FlutterEngine(ctx)
|
engine = FlutterEngine(ctx)
|
||||||
|
|
||||||
|
|
@ -82,6 +108,34 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Move this to a separate NotificationManager class
|
||||||
|
override fun showNotification(title: String, content: String) {
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.notification_icon)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setTicker(title)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
if (isIgnoringBatteryOptimizations()) {
|
||||||
|
foregroundFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
setForegroundAsync(
|
||||||
|
ForegroundInfo(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
notification,
|
||||||
|
FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setForegroundAsync(ForegroundInfo(NOTIFICATION_ID, notification))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
return
|
return
|
||||||
|
|
@ -95,6 +149,8 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitForForegroundPromotion()
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
complete(Result.failure())
|
complete(Result.failure())
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
@ -135,6 +191,32 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
engine?.destroy()
|
engine?.destroy()
|
||||||
engine = null
|
engine = null
|
||||||
flutterApi = null
|
flutterApi = null
|
||||||
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
waitForForegroundPromotion()
|
||||||
completionHandler.set(success)
|
completionHandler.set(success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the app is ignoring battery optimizations
|
||||||
|
*/
|
||||||
|
private fun isIgnoringBatteryOptimizations(): Boolean {
|
||||||
|
val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
return powerManager.isIgnoringBatteryOptimizations(ctx.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls to setForegroundAsync() that do not complete before completion of a ListenableWorker will signal an IllegalStateException
|
||||||
|
* https://android-review.googlesource.com/c/platform/frameworks/support/+/1262743
|
||||||
|
* Wait for a short period of time for the foreground promotion to complete before completing the worker
|
||||||
|
*/
|
||||||
|
private fun waitForForegroundPromotion() {
|
||||||
|
val foregroundFuture = this.foregroundFuture
|
||||||
|
if (foregroundFuture != null && !foregroundFuture.isCancelled && !foregroundFuture.isDone) {
|
||||||
|
try {
|
||||||
|
foregroundFuture.get(500, TimeUnit.MILLISECONDS)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ignored, there is nothing to be done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||||
|
|
||||||
|
package app.alextran.immich.connectivity
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMethodCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
private object ConnectivityPigeonUtils {
|
||||||
|
|
||||||
|
fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
return if (exception is FlutterError) {
|
||||||
|
listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||||
|
* @property code The error code.
|
||||||
|
* @property message The error message.
|
||||||
|
* @property details The error details. Must be a datatype supported by the api codec.
|
||||||
|
*/
|
||||||
|
class FlutterError (
|
||||||
|
val code: String,
|
||||||
|
override val message: String? = null,
|
||||||
|
val details: Any? = null
|
||||||
|
) : Throwable()
|
||||||
|
|
||||||
|
enum class NetworkCapability(val raw: Int) {
|
||||||
|
CELLULAR(0),
|
||||||
|
WIFI(1),
|
||||||
|
VPN(2),
|
||||||
|
UNMETERED(3);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofRaw(raw: Int): NetworkCapability? {
|
||||||
|
return values().firstOrNull { it.raw == raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private open class ConnectivityPigeonCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return when (type) {
|
||||||
|
129.toByte() -> {
|
||||||
|
return (readValue(buffer) as Long?)?.let {
|
||||||
|
NetworkCapability.ofRaw(it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is NetworkCapability -> {
|
||||||
|
stream.write(129)
|
||||||
|
writeValue(stream, value.raw)
|
||||||
|
}
|
||||||
|
else -> super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface ConnectivityApi {
|
||||||
|
fun getCapabilities(): List<NetworkCapability>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by ConnectivityApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
ConnectivityPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val taskQueue = binaryMessenger.makeBackgroundTaskQueue()
|
||||||
|
run {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package app.alextran.immich.connectivity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
|
||||||
|
class ConnectivityApiImpl(context: Context) : ConnectivityApi {
|
||||||
|
private val connectivityManager =
|
||||||
|
context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
private val wifiManager =
|
||||||
|
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
|
override fun getCapabilities(): List<NetworkCapability> {
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||||
|
?: return emptyList()
|
||||||
|
|
||||||
|
val hasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||||
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||||
|
val hasCellular = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
val hasVpn = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
||||||
|
val isUnmetered = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
|
|
||||||
|
return buildList {
|
||||||
|
if (hasWifi) add(NetworkCapability.WIFI)
|
||||||
|
if (hasCellular) add(NetworkCapability.CELLULAR)
|
||||||
|
if (hasVpn) {
|
||||||
|
add(NetworkCapability.VPN)
|
||||||
|
if (!hasWifi && !hasCellular) {
|
||||||
|
if (wifiManager.isWifiEnabled) add(NetworkCapability.WIFI)
|
||||||
|
// If VPN is active, but neither WIFI nor CELLULAR is reported as active,
|
||||||
|
// assume CELLULAR if WIFI is not enabled
|
||||||
|
else add(NetworkCapability.CELLULAR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isUnmetered) add(NetworkCapability.UNMETERED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; };
|
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */; };
|
||||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
||||||
|
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
||||||
|
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
||||||
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
||||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
||||||
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
|
@ -97,6 +99,8 @@
|
||||||
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
|
||||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||||
|
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
||||||
|
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
||||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
||||||
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
|
@ -129,8 +133,6 @@
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -243,6 +245,7 @@
|
||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||||
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
||||||
|
|
@ -271,6 +274,15 @@
|
||||||
path = Background;
|
path = Background;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
B25D37792E72CA15008B6CA7 /* Connectivity */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */,
|
||||||
|
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */,
|
||||||
|
);
|
||||||
|
path = Connectivity;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -507,10 +519,14 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
|
@ -539,10 +555,14 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
|
@ -558,7 +578,9 @@
|
||||||
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
|
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
|
||||||
|
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
|
||||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
||||||
|
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
|
||||||
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */,
|
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */,
|
||||||
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */,
|
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */,
|
||||||
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */,
|
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ class BackgroundWorkerFgHostApiSetup {
|
||||||
/// 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 BackgroundWorkerBgHostApi {
|
protocol BackgroundWorkerBgHostApi {
|
||||||
func onInitialized() throws
|
func onInitialized() throws
|
||||||
|
func showNotification(title: String, content: String) throws
|
||||||
func close() throws
|
func close() throws
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +137,22 @@ class BackgroundWorkerBgHostApiSetup {
|
||||||
} else {
|
} else {
|
||||||
onInitializedChannel.setMessageHandler(nil)
|
onInitializedChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
|
let showNotificationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
showNotificationChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let titleArg = args[0] as! String
|
||||||
|
let contentArg = args[1] as! String
|
||||||
|
do {
|
||||||
|
try api.showNotification(title: titleArg, content: contentArg)
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotificationChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
closeChannel.setMessageHandler { _, reply in
|
closeChannel.setMessageHandler { _, reply in
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showNotification(title: String, content: String) throws {
|
||||||
|
// No-op on iOS for the time being
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the currently running background task, either due to timeout or external request.
|
* Cancels the currently running background task, either due to timeout or external request.
|
||||||
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
||||||
|
|
|
||||||
129
mobile/ios/Runner/Connectivity/Connectivity.g.swift
Normal file
129
mobile/ios/Runner/Connectivity/Connectivity.g.swift
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
import Flutter
|
||||||
|
#elseif os(macOS)
|
||||||
|
import FlutterMacOS
|
||||||
|
#else
|
||||||
|
#error("Unsupported platform.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||||
|
return [result]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wrapError(_ error: Any) -> [Any?] {
|
||||||
|
if let pigeonError = error as? PigeonError {
|
||||||
|
return [
|
||||||
|
pigeonError.code,
|
||||||
|
pigeonError.message,
|
||||||
|
pigeonError.details,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if let flutterError = error as? FlutterError {
|
||||||
|
return [
|
||||||
|
flutterError.code,
|
||||||
|
flutterError.message,
|
||||||
|
flutterError.details,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"\(error)",
|
||||||
|
"\(type(of: error))",
|
||||||
|
"Stacktrace: \(Thread.callStackSymbols)",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isNullish(_ value: Any?) -> Bool {
|
||||||
|
return value is NSNull || value == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||||
|
if value is NSNull { return nil }
|
||||||
|
return value as! T?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum NetworkCapability: Int {
|
||||||
|
case cellular = 0
|
||||||
|
case wifi = 1
|
||||||
|
case vpn = 2
|
||||||
|
case unmetered = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConnectivityPigeonCodecReader: FlutterStandardReader {
|
||||||
|
override func readValue(ofType type: UInt8) -> Any? {
|
||||||
|
switch type {
|
||||||
|
case 129:
|
||||||
|
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
|
||||||
|
if let enumResultAsInt = enumResultAsInt {
|
||||||
|
return NetworkCapability(rawValue: enumResultAsInt)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return super.readValue(ofType: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConnectivityPigeonCodecWriter: FlutterStandardWriter {
|
||||||
|
override func writeValue(_ value: Any) {
|
||||||
|
if let value = value as? NetworkCapability {
|
||||||
|
super.writeByte(129)
|
||||||
|
super.writeValue(value.rawValue)
|
||||||
|
} else {
|
||||||
|
super.writeValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConnectivityPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||||
|
override func reader(with data: Data) -> FlutterStandardReader {
|
||||||
|
return ConnectivityPigeonCodecReader(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||||
|
return ConnectivityPigeonCodecWriter(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
class ConnectivityApiSetup {
|
||||||
|
static var codec: FlutterStandardMessageCodec { ConnectivityPigeonCodec.shared }
|
||||||
|
/// Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`.
|
||||||
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") {
|
||||||
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
|
#if os(iOS)
|
||||||
|
let taskQueue = binaryMessenger.makeBackgroundTaskQueue?()
|
||||||
|
#else
|
||||||
|
let taskQueue: FlutterTaskQueue? = nil
|
||||||
|
#endif
|
||||||
|
let getCapabilitiesChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getCapabilitiesChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift
Normal file
6
mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
class ConnectivityApiImpl: ConnectivityApi {
|
||||||
|
func getCapabilities() throws -> [NetworkCapability] {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/generated/intl_keys.g.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||||
|
|
@ -13,11 +18,13 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/auth.service.dart';
|
import 'package:immich_mobile/services/auth.service.dart';
|
||||||
import 'package:immich_mobile/services/localization.service.dart';
|
import 'package:immich_mobile/services/localization.service.dart';
|
||||||
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
import 'package:immich_mobile/services/upload.service.dart';
|
import 'package:immich_mobile/services/upload.service.dart';
|
||||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||||
|
|
@ -42,6 +49,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
final Drift _drift;
|
final Drift _drift;
|
||||||
final DriftLogger _driftLogger;
|
final DriftLogger _driftLogger;
|
||||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||||
|
final CancellationToken _cancellationToken = CancellationToken();
|
||||||
final Logger _logger = Logger('BackgroundWorkerBgService');
|
final Logger _logger = Logger('BackgroundWorkerBgService');
|
||||||
|
|
||||||
bool _isCleanedUp = false;
|
bool _isCleanedUp = false;
|
||||||
|
|
@ -87,6 +95,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
|
|
||||||
configureFileDownloaderNotifications();
|
configureFileDownloaderNotifications();
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
await _backgroundHostApi.showNotification(
|
||||||
|
IntlKeys.uploading_media.t(),
|
||||||
|
IntlKeys.backup_background_service_in_progress_notification.t(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Notify the host that the background worker service has been initialized and is ready to use
|
// Notify the host that the background worker service has been initialized and is ready to use
|
||||||
_backgroundHostApi.onInitialized();
|
_backgroundHostApi.onInitialized();
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
|
|
@ -102,7 +117,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
final sw = Stopwatch()..start();
|
final sw = Stopwatch()..start();
|
||||||
|
|
||||||
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
||||||
await _handleBackup(processBulk: false);
|
await _handleBackup();
|
||||||
|
|
||||||
sw.stop();
|
sw.stop();
|
||||||
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
||||||
|
|
@ -155,9 +170,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_isCleanedUp = true;
|
_isCleanedUp = true;
|
||||||
|
_cancellationToken.cancel();
|
||||||
_logger.info("Cleaning up background worker");
|
_logger.info("Cleaning up background worker");
|
||||||
final cleanupFutures = [
|
final cleanupFutures = [
|
||||||
workerManager.dispose(),
|
workerManager.dispose().catchError((_) async {
|
||||||
|
// Discard any errors on the dispose call
|
||||||
|
return;
|
||||||
|
}),
|
||||||
_drift.close(),
|
_drift.close(),
|
||||||
_driftLogger.close(),
|
_driftLogger.close(),
|
||||||
_ref.read(backgroundSyncProvider).cancel(),
|
_ref.read(backgroundSyncProvider).cancel(),
|
||||||
|
|
@ -175,7 +194,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleBackup({bool processBulk = true}) async {
|
Future<void> _handleBackup() async {
|
||||||
if (!_isBackupEnabled || _isCleanedUp) {
|
if (!_isBackupEnabled || _isCleanedUp) {
|
||||||
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
||||||
return;
|
return;
|
||||||
|
|
@ -189,19 +208,22 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processBulk) {
|
|
||||||
_logger.info("[_handleBackup 4] Resume backup from background");
|
_logger.info("[_handleBackup 4] Resume backup from background");
|
||||||
|
if (Platform.isIOS) {
|
||||||
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id);
|
final canPing = await _ref.read(serverInfoServiceProvider).ping();
|
||||||
if (activeTask.isNotEmpty) {
|
if (!canPing) {
|
||||||
_logger.info("[_handleBackup 5] Resuming backup for active tasks from background");
|
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
|
||||||
await _ref.read(uploadServiceProvider).resumeBackup();
|
return;
|
||||||
} else {
|
|
||||||
_logger.info("[_handleBackup 6] Starting serial backup for new tasks from background");
|
|
||||||
await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
|
||||||
|
|
||||||
|
return _ref
|
||||||
|
.read(uploadServiceProvider)
|
||||||
|
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
||||||
|
|
|
||||||
8
mobile/lib/extensions/network_capability_extensions.dart
Normal file
8
mobile/lib/extensions/network_capability_extensions.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||||
|
|
||||||
|
extension NetworkCapabilitiesGetters on List<NetworkCapability> {
|
||||||
|
bool get hasCellular => contains(NetworkCapability.cellular);
|
||||||
|
bool get hasWifi => contains(NetworkCapability.wifi);
|
||||||
|
bool get hasVpn => contains(NetworkCapability.vpn);
|
||||||
|
bool get isUnmetered => contains(NetworkCapability.unmetered);
|
||||||
|
}
|
||||||
|
|
@ -46,7 +46,7 @@ void main() async {
|
||||||
await Bootstrap.initDomain(isar, drift, logDb);
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await initApp();
|
await initApp();
|
||||||
// Warm-up isolate pool for worker manager
|
// Warm-up isolate pool for worker manager
|
||||||
await workerManager.init(dynamicSpawning: true);
|
await workerManager.init(dynamicSpawning: false);
|
||||||
await migrateDatabaseIfNeeded(isar, drift);
|
await migrateDatabaseIfNeeded(isar, drift);
|
||||||
HttpSSLOptions.apply();
|
HttpSSLOptions.apply();
|
||||||
|
|
||||||
|
|
|
||||||
23
mobile/lib/platform/background_worker_api.g.dart
generated
23
mobile/lib/platform/background_worker_api.g.dart
generated
|
|
@ -142,6 +142,29 @@ class BackgroundWorkerBgHostApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> showNotification(String title, String content) async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[title, content]);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
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<void> close() async {
|
Future<void> close() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix';
|
||||||
|
|
|
||||||
87
mobile/lib/platform/connectivity_api.g.dart
generated
Normal file
87
mobile/lib/platform/connectivity_api.g.dart
generated
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
PlatformException _createConnectionError(String channelName) {
|
||||||
|
return PlatformException(
|
||||||
|
code: 'channel-error',
|
||||||
|
message: 'Unable to establish connection on channel: "$channelName".',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NetworkCapability { cellular, wifi, vpn, unmetered }
|
||||||
|
|
||||||
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
|
const _PigeonCodec();
|
||||||
|
@override
|
||||||
|
void writeValue(WriteBuffer buffer, Object? value) {
|
||||||
|
if (value is int) {
|
||||||
|
buffer.putUint8(4);
|
||||||
|
buffer.putInt64(value);
|
||||||
|
} else if (value is NetworkCapability) {
|
||||||
|
buffer.putUint8(129);
|
||||||
|
writeValue(buffer, value.index);
|
||||||
|
} else {
|
||||||
|
super.writeValue(buffer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
|
switch (type) {
|
||||||
|
case 129:
|
||||||
|
final int? value = readValue(buffer) as int?;
|
||||||
|
return value == null ? null : NetworkCapability.values[value];
|
||||||
|
default:
|
||||||
|
return super.readValueOfType(type, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectivityApi {
|
||||||
|
/// Constructor for [ConnectivityApi]. The [binaryMessenger] named argument is
|
||||||
|
/// available for dependency injection. If it is left null, the default
|
||||||
|
/// BinaryMessenger will be used which routes to the host platform.
|
||||||
|
ConnectivityApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||||
|
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||||
|
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
|
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||||
|
|
||||||
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
|
Future<List<NetworkCapability>> getCapabilities() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
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 if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<NetworkCapability>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
||||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||||
|
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/thumbnail_api.g.dart';
|
import 'package:immich_mobile/platform/thumbnail_api.g.dart';
|
||||||
|
|
||||||
|
|
@ -8,4 +9,6 @@ final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgServ
|
||||||
|
|
||||||
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
||||||
|
|
||||||
|
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
|
||||||
|
|
||||||
final thumbnailApi = ThumbnailApi();
|
final thumbnailApi = ThumbnailApi();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,21 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
class UploadTaskWithFile {
|
||||||
|
final File file;
|
||||||
|
final UploadTask task;
|
||||||
|
|
||||||
|
const UploadTaskWithFile({required this.file, required this.task});
|
||||||
|
}
|
||||||
|
|
||||||
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
||||||
|
|
||||||
|
|
@ -31,7 +45,7 @@ class UploadRepository {
|
||||||
return FileDownloader().enqueue(task);
|
return FileDownloader().enqueue(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> enqueueBackgroundAll(List<UploadTask> tasks) {
|
Future<List<bool>> enqueueBackgroundAll(List<UploadTask> tasks) {
|
||||||
return FileDownloader().enqueueAll(tasks);
|
return FileDownloader().enqueueAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,4 +88,53 @@ class UploadRepository {
|
||||||
Paused: ${pausedTasks.length}
|
Paused: ${pausedTasks.length}
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async {
|
||||||
|
final httpClient = Client();
|
||||||
|
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
|
|
||||||
|
Logger logger = Logger('UploadRepository');
|
||||||
|
for (final candidate in tasks) {
|
||||||
|
if (cancelToken.isCancelled) {
|
||||||
|
logger.warning("Backup was cancelled by the user");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final fileStream = candidate.file.openRead();
|
||||||
|
final assetRawUploadData = MultipartFile(
|
||||||
|
"assetData",
|
||||||
|
fileStream,
|
||||||
|
candidate.file.lengthSync(),
|
||||||
|
filename: candidate.task.filename,
|
||||||
|
);
|
||||||
|
|
||||||
|
final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets'));
|
||||||
|
|
||||||
|
baseRequest.headers.addAll(candidate.task.headers);
|
||||||
|
baseRequest.fields.addAll(candidate.task.fields);
|
||||||
|
baseRequest.files.add(assetRawUploadData);
|
||||||
|
|
||||||
|
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken);
|
||||||
|
|
||||||
|
final responseBody = jsonDecode(await response.stream.bytesToString());
|
||||||
|
|
||||||
|
if (![200, 201].contains(response.statusCode)) {
|
||||||
|
final error = responseBody;
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"Error(${error['statusCode']}) uploading ${candidate.task.filename} | Created on ${candidate.task.fields["fileCreatedAt"]} | ${error['error']}",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} on CancelledException {
|
||||||
|
logger.warning("Backup was cancelled by the user");
|
||||||
|
break;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logger.warning("Error backup asset: ${error.toString()}: $stackTrace");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,15 @@ class ServerInfoService {
|
||||||
|
|
||||||
const ServerInfoService(this._apiService);
|
const ServerInfoService(this._apiService);
|
||||||
|
|
||||||
|
Future<bool> ping() async {
|
||||||
|
try {
|
||||||
|
await _apiService.serverInfoApi.pingServer().timeout(const Duration(seconds: 5));
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<ServerDiskInfo?> getDiskInfo() async {
|
Future<ServerDiskInfo?> getDiskInfo() async {
|
||||||
try {
|
try {
|
||||||
final dto = await _apiService.serverInfoApi.getStorage();
|
final dto = await _apiService.serverInfoApi.getStorage();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
|
@ -19,6 +20,7 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
final uploadServiceProvider = Provider((ref) {
|
final uploadServiceProvider = Provider((ref) {
|
||||||
|
|
@ -51,6 +53,7 @@ class UploadService {
|
||||||
final StorageRepository _storageRepository;
|
final StorageRepository _storageRepository;
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final AppSettingsService _appSettingsService;
|
final AppSettingsService _appSettingsService;
|
||||||
|
final Logger _logger = Logger('UploadService');
|
||||||
|
|
||||||
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
|
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
|
||||||
final StreamController<TaskProgressUpdate> _taskProgressController = StreamController<TaskProgressUpdate>.broadcast();
|
final StreamController<TaskProgressUpdate> _taskProgressController = StreamController<TaskProgressUpdate>.broadcast();
|
||||||
|
|
@ -78,7 +81,7 @@ class UploadService {
|
||||||
_taskProgressController.close();
|
_taskProgressController.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> enqueueTasks(List<UploadTask> tasks) {
|
Future<List<bool>> enqueueTasks(List<UploadTask> tasks) {
|
||||||
return _uploadRepository.enqueueBackgroundAll(tasks);
|
return _uploadRepository.enqueueBackgroundAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +141,6 @@ class UploadService {
|
||||||
}
|
}
|
||||||
|
|
||||||
final batch = candidates.skip(i).take(batchSize).toList();
|
final batch = candidates.skip(i).take(batchSize).toList();
|
||||||
|
|
||||||
List<UploadTask> tasks = [];
|
List<UploadTask> tasks = [];
|
||||||
for (final asset in batch) {
|
for (final asset in batch) {
|
||||||
final task = await _getUploadTask(asset);
|
final task = await _getUploadTask(asset);
|
||||||
|
|
@ -156,9 +158,7 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enqueue All does not work from the background on Android yet. This method is a temporary workaround
|
Future<void> startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async {
|
||||||
// that enqueues tasks one by one.
|
|
||||||
Future<void> startBackupSerial(String userId) async {
|
|
||||||
await _storageRepository.clearCache();
|
await _storageRepository.clearCache();
|
||||||
|
|
||||||
shouldAbortQueuingTasks = false;
|
shouldAbortQueuingTasks = false;
|
||||||
|
|
@ -168,14 +168,29 @@ class UploadService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final asset in candidates) {
|
const batchSize = 100;
|
||||||
if (shouldAbortQueuingTasks) {
|
for (int i = 0; i < candidates.length; i += batchSize) {
|
||||||
|
if (shouldAbortQueuingTasks || token.isCancelled) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final task = await _getUploadTask(asset);
|
final batch = candidates.skip(i).take(batchSize).toList();
|
||||||
|
List<UploadTaskWithFile> tasks = [];
|
||||||
|
for (final asset in batch) {
|
||||||
|
final requireWifi = _shouldRequireWiFi(asset);
|
||||||
|
if (requireWifi && !hasWifi) {
|
||||||
|
_logger.warning('Skipping upload for ${asset.id} because it requires WiFi');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final task = await _getUploadTaskWithFile(asset);
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
await _uploadRepository.enqueueBackground(task);
|
tasks.add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
|
||||||
|
await _uploadRepository.backupWithDartClient(tasks, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,6 +257,42 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UploadTaskWithFile?> _getUploadTaskWithFile(LocalAsset asset) async {
|
||||||
|
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||||
|
if (file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name;
|
||||||
|
|
||||||
|
String metadata = UploadTaskMetadata(
|
||||||
|
localAssetId: asset.id,
|
||||||
|
isLivePhotos: entity.isLivePhoto,
|
||||||
|
livePhotoVideoId: '',
|
||||||
|
).toJson();
|
||||||
|
|
||||||
|
return UploadTaskWithFile(
|
||||||
|
file: file,
|
||||||
|
task: await buildUploadTask(
|
||||||
|
file,
|
||||||
|
createdAt: asset.createdAt,
|
||||||
|
modifiedAt: asset.updatedAt,
|
||||||
|
originalFileName: originalFileName,
|
||||||
|
deviceAssetId: asset.id,
|
||||||
|
metadata: metadata,
|
||||||
|
group: "group",
|
||||||
|
priority: 0,
|
||||||
|
isFavorite: asset.isFavorite,
|
||||||
|
requiresWiFi: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<UploadTask?> _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async {
|
Future<UploadTask?> _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async {
|
||||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ pigeon:
|
||||||
dart run pigeon --input pigeon/native_sync_api.dart
|
dart run pigeon --input pigeon/native_sync_api.dart
|
||||||
dart run pigeon --input pigeon/thumbnail_api.dart
|
dart run pigeon --input pigeon/thumbnail_api.dart
|
||||||
dart run pigeon --input pigeon/background_worker_api.dart
|
dart run pigeon --input pigeon/background_worker_api.dart
|
||||||
|
dart run pigeon --input pigeon/connectivity_api.dart
|
||||||
dart format lib/platform/native_sync_api.g.dart
|
dart format lib/platform/native_sync_api.g.dart
|
||||||
dart format lib/platform/thumbnail_api.g.dart
|
dart format lib/platform/thumbnail_api.g.dart
|
||||||
dart format lib/platform/background_worker_api.g.dart
|
dart format lib/platform/background_worker_api.g.dart
|
||||||
|
dart format lib/platform/connectivity_api.g.dart
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
dart run build_runner watch --delete-conflicting-outputs
|
dart run build_runner watch --delete-conflicting-outputs
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ abstract class BackgroundWorkerBgHostApi {
|
||||||
// required platform channels to notify the native side to start the background upload
|
// required platform channels to notify the native side to start the background upload
|
||||||
void onInitialized();
|
void onInitialized();
|
||||||
|
|
||||||
|
void showNotification(String title, String content);
|
||||||
|
|
||||||
// Called from the background flutter engine to request the native side to cleanup
|
// Called from the background flutter engine to request the native side to cleanup
|
||||||
void close();
|
void close();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
mobile/pigeon/connectivity_api.dart
Normal file
20
mobile/pigeon/connectivity_api.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:pigeon/pigeon.dart';
|
||||||
|
|
||||||
|
@ConfigurePigeon(
|
||||||
|
PigeonOptions(
|
||||||
|
dartOut: 'lib/platform/connectivity_api.g.dart',
|
||||||
|
swiftOut: 'ios/Runner/Connectivity/Connectivity.g.swift',
|
||||||
|
swiftOptions: SwiftOptions(includeErrorClass: false),
|
||||||
|
kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt',
|
||||||
|
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.connectivity'),
|
||||||
|
dartOptions: DartOptions(),
|
||||||
|
dartPackageName: 'immich_mobile',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
enum NetworkCapability { cellular, wifi, vpn, unmetered }
|
||||||
|
|
||||||
|
@HostApi()
|
||||||
|
abstract class ConnectivityApi {
|
||||||
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
|
List<NetworkCapability> getCapabilities();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue