mirror of
https://github.com/immich-app/immich.git
synced 2025-12-17 09:13:17 +03:00
Compare commits
2 Commits
feat/locat
...
fix/restri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9029ec5bb6 | ||
|
|
02456a148e |
@@ -43,8 +43,8 @@ class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, ImmichPl
|
|||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
super.onAttachedToEngine(binding)
|
super.onAttachedToEngine(binding)
|
||||||
checkAndEnforceBackgroundLock(binding.applicationContext)
|
|
||||||
engineCount.incrementAndGet()
|
engineCount.incrementAndGet()
|
||||||
|
checkAndEnforceBackgroundLock(binding.applicationContext)
|
||||||
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
|
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -295,12 +295,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun onAndroidUpload(callback: (Result<Unit>) -> Unit)
|
fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||||
{
|
{
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
|
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
channel.send(null) {
|
channel.send(listOf(maxMinutesArg)) {
|
||||||
if (it is List<*>) {
|
if (it is List<*>) {
|
||||||
if (it.size > 1) {
|
if (it.size > 1) {
|
||||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
* This method acts as a bridge between the native Android background task system and Flutter.
|
* This method acts as a bridge between the native Android background task system and Flutter.
|
||||||
*/
|
*/
|
||||||
override fun onInitialized() {
|
override fun onInitialized() {
|
||||||
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to a separate NotificationManager class
|
// TODO: Move this to a separate NotificationManager class
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import android.provider.MediaStore
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import io.flutter.embedding.engine.FlutterEngineCache
|
import io.flutter.embedding.engine.FlutterEngineCache
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -18,6 +20,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
|
|
||||||
override fun enable() {
|
override fun enable() {
|
||||||
enqueueMediaObserver(ctx)
|
enqueueMediaObserver(ctx)
|
||||||
|
enqueuePeriodicWorker(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveNotificationMessage(title: String, body: String) {
|
override fun saveNotificationMessage(title: String, body: String) {
|
||||||
@@ -27,12 +30,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
override fun configure(settings: BackgroundWorkerSettings) {
|
override fun configure(settings: BackgroundWorkerSettings) {
|
||||||
BackgroundWorkerPreferences(ctx).updateSettings(settings)
|
BackgroundWorkerPreferences(ctx).updateSettings(settings)
|
||||||
enqueueMediaObserver(ctx)
|
enqueueMediaObserver(ctx)
|
||||||
|
enqueuePeriodicWorker(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disable() {
|
override fun disable() {
|
||||||
WorkManager.getInstance(ctx).apply {
|
WorkManager.getInstance(ctx).apply {
|
||||||
cancelUniqueWork(OBSERVER_WORKER_NAME)
|
cancelUniqueWork(OBSERVER_WORKER_NAME)
|
||||||
cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||||
|
cancelUniqueWork(PERIODIC_WORKER_NAME)
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Cancelled background upload tasks")
|
Log.i(TAG, "Cancelled background upload tasks")
|
||||||
}
|
}
|
||||||
@@ -40,6 +45,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
||||||
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
||||||
|
private const val PERIODIC_WORKER_NAME = "immich/PeriodicBackgroundWorkerV1"
|
||||||
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
|
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
|
||||||
|
|
||||||
|
|
||||||
@@ -55,7 +61,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
setRequiresCharging(settings.requiresCharging)
|
setRequiresCharging(settings.requiresCharging)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
val work = OneTimeWorkRequestBuilder<MediaObserver>()
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.build()
|
.build()
|
||||||
WorkManager.getInstance(ctx)
|
WorkManager.getInstance(ctx)
|
||||||
@@ -67,10 +73,30 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun enqueuePeriodicWorker(ctx: Context) {
|
||||||
|
val settings = BackgroundWorkerPreferences(ctx).getSettings()
|
||||||
|
val constraints = Constraints.Builder().apply {
|
||||||
|
setRequiresCharging(settings.requiresCharging)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val work =
|
||||||
|
PeriodicWorkRequestBuilder<PeriodicWorker>(
|
||||||
|
1,
|
||||||
|
TimeUnit.HOURS,
|
||||||
|
15,
|
||||||
|
TimeUnit.MINUTES
|
||||||
|
).setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(ctx)
|
||||||
|
.enqueueUniquePeriodicWork(PERIODIC_WORKER_NAME, ExistingPeriodicWorkPolicy.UPDATE, work)
|
||||||
|
|
||||||
|
Log.i(TAG, "Enqueued periodic background worker with name: $PERIODIC_WORKER_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
fun enqueueBackgroundWorker(ctx: Context) {
|
fun enqueueBackgroundWorker(ctx: Context) {
|
||||||
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
||||||
|
val work = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||||
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package app.alextran.immich.background
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
|
||||||
|
class PeriodicWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
Log.i("PeriodicWorker", "Periodic worker triggered, starting background worker")
|
||||||
|
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -295,7 +295,7 @@ class BackgroundWorkerBgHostApiSetup {
|
|||||||
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||||
protocol BackgroundWorkerFlutterApiProtocol {
|
protocol BackgroundWorkerFlutterApiProtocol {
|
||||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
}
|
}
|
||||||
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
||||||
@@ -326,10 +326,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
|
||||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
channel.sendMessage(nil) { response in
|
channel.sendMessage([maxMinutesArg] as [Any?]) { response in
|
||||||
guard let listResponse = response as? [Any?] else {
|
guard let listResponse = response as? [Any?] else {
|
||||||
completion(.failure(createConnectionError(withChannelName: channelName)))
|
completion(.failure(createConnectionError(withChannelName: channelName)))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -122,46 +122,54 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onAndroidUpload() async {
|
Future<void> onAndroidUpload(int? maxMinutes) async {
|
||||||
_logger.info('Android background processing started');
|
final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||||
final sw = Stopwatch()..start();
|
final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
|
||||||
try {
|
return _backgroundLoop(
|
||||||
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) {
|
hashTimeout: hashTimeout,
|
||||||
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
backupTimeout: backupTimeout,
|
||||||
return;
|
debugLabel: 'Android background upload',
|
||||||
}
|
);
|
||||||
await _handleBackup();
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.severe("Failed to complete Android background processing", error, stack);
|
|
||||||
} finally {
|
|
||||||
sw.stop();
|
|
||||||
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
await _cleanup();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||||
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
|
final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||||
|
final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
|
||||||
|
return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _backgroundLoop({
|
||||||
|
required Duration hashTimeout,
|
||||||
|
required Duration? backupTimeout,
|
||||||
|
required String debugLabel,
|
||||||
|
}) async {
|
||||||
|
_logger.info(
|
||||||
|
'$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m',
|
||||||
|
);
|
||||||
final sw = Stopwatch()..start();
|
final sw = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
if (!await _syncAssets(hashTimeout: hashTimeout)) {
|
||||||
if (!await _syncAssets(hashTimeout: timeout)) {
|
|
||||||
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupFuture = _handleBackup();
|
final backupFuture = _handleBackup();
|
||||||
if (maxSeconds != null) {
|
if (backupTimeout != null) {
|
||||||
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
|
await backupFuture.timeout(
|
||||||
|
backupTimeout,
|
||||||
|
onTimeout: () {
|
||||||
|
_cancellationToken.cancel();
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await backupFuture;
|
await backupFuture;
|
||||||
}
|
}
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Failed to complete iOS background upload", error, stack);
|
_logger.severe("Failed to complete $debugLabel", error, stack);
|
||||||
} finally {
|
} finally {
|
||||||
sw.stop();
|
sw.stop();
|
||||||
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
_logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s");
|
||||||
await _cleanup();
|
await _cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
mobile/lib/platform/background_worker_api.g.dart
generated
10
mobile/lib/platform/background_worker_api.g.dart
generated
@@ -273,7 +273,7 @@ abstract class BackgroundWorkerFlutterApi {
|
|||||||
|
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|
||||||
Future<void> onAndroidUpload();
|
Future<void> onAndroidUpload(int? maxMinutes);
|
||||||
|
|
||||||
Future<void> cancel();
|
Future<void> cancel();
|
||||||
|
|
||||||
@@ -327,8 +327,14 @@ abstract class BackgroundWorkerFlutterApi {
|
|||||||
pigeonVar_channel.setMessageHandler(null);
|
pigeonVar_channel.setMessageHandler(null);
|
||||||
} else {
|
} else {
|
||||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||||
|
assert(
|
||||||
|
message != null,
|
||||||
|
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload was null.',
|
||||||
|
);
|
||||||
|
final List<Object?> args = (message as List<Object?>?)!;
|
||||||
|
final int? arg_maxMinutes = (args[0] as int?);
|
||||||
try {
|
try {
|
||||||
await api.onAndroidUpload();
|
await api.onAndroidUpload(arg_maxMinutes);
|
||||||
return wrapResponse(empty: true);
|
return wrapResponse(empty: true);
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
return wrapResponse(error: e);
|
return wrapResponse(error: e);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ abstract class BackgroundWorkerFlutterApi {
|
|||||||
|
|
||||||
// Android Only: Called when the Android background upload is triggered
|
// Android Only: Called when the Android background upload is triggered
|
||||||
@async
|
@async
|
||||||
void onAndroidUpload();
|
void onAndroidUpload(int? maxMinutes);
|
||||||
|
|
||||||
@async
|
@async
|
||||||
void cancel();
|
void cancel();
|
||||||
|
|||||||
Reference in New Issue
Block a user