mirror of
https://github.com/immich-app/immich.git
synced 2025-01-11 06:10:28 +02:00
feat(mobile): iOS background sync (#1758)
* first run of getting background sync working in iOS * got background sync calling into flutter * added background task * added necessary sync files * fixed some names and added more implementations * got as far as Hive.initFlutter * brute force got to await Hive.initFlutter * lots of print statements to figure out where execution is failing, and its failing at the root asset bundle in the localization.dart service * first time working, got plugins registered * removed broken cleanup code * refactored * linters * now can pass user settings * background service plugin uses app background processing instead of fetch * renamed backgroundFetch to backgroundProcessing to make it clearer * don't use max delay * adds fetch back in * fixes require charging default values and backup controller page * fixes background fetch * fixes ios not importing photos * guarded path provider ios * lint * adds max tries for heartbeat to work in iOS * fail after seconds * timeout instead of fail after seconds * removes release lock from system stop * restores checkLockReleasedWithHeartbeat to Future<void> * removes max tries from acquire lock * fixes lock timeout with iOS * restored for loop * adds comments, made the AppRefresh task only run while not requiring network or charge * fixed compile issue * now both are registered and added better comments. also added ability for task to cancel itself * added the podfile and pubspec * added backup diagnostics to IOS and removed iOS ignored backup options and fixed network connectivity always required * Added Alex's dev team * styled debug list item, fixed refresh task not set bug, fixed enable / disable background service on platform channel --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
2cf42e867c
commit
87fea29e32
1
mobile/.gitignore
vendored
1
mobile/.gitignore
vendored
@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
**/ios/
|
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins
|
.flutter-plugins
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
|
@ -24,6 +24,8 @@ PODS:
|
|||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- path_provider_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- photo_manager (2.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@ -55,6 +57,7 @@ DEPENDENCIES:
|
|||||||
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
|
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||||
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
||||||
@ -90,6 +93,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||||
|
path_provider_ios:
|
||||||
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
share_plus:
|
share_plus:
|
||||||
@ -117,6 +122,7 @@ SPEC CHECKSUMS:
|
|||||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||||
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; };
|
||||||
|
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
@ -34,6 +36,8 @@
|
|||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = "<group>"; };
|
||||||
|
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
@ -80,6 +84,15 @@
|
|||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
65DD438629917FAD0047FFA8 /* BackgroundSync */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */,
|
||||||
|
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */,
|
||||||
|
);
|
||||||
|
path = BackgroundSync;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -113,6 +126,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
65DD438629917FAD0047FFA8 /* BackgroundSync */,
|
||||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
@ -275,8 +289,10 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -363,7 +379,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 86;
|
CURRENT_PROJECT_VERSION = 86;
|
||||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -372,6 +387,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -499,7 +515,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 86;
|
CURRENT_PROJECT_VERSION = 86;
|
||||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -508,6 +523,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -527,7 +543,6 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 86;
|
CURRENT_PROJECT_VERSION = 86;
|
||||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -536,6 +551,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -43,11 +43,13 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
disableMainThreadChecker = "YES"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
@ -1,13 +1,33 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
import BackgroundTasks
|
||||||
|
import path_provider_ios
|
||||||
|
import photo_manager
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
BackgroundServicePlugin.registerBackgroundProcessing()
|
||||||
|
|
||||||
|
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
|
||||||
|
|
||||||
|
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
||||||
|
if !registry.hasPlugin("org.cocoapods.path-provider-ios") {
|
||||||
|
FLTPathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-ios")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !registry.hasPlugin("org.cocoapods.photo-manager") {
|
||||||
|
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
6
mobile/ios/Runner/Assets.xcassets/Contents.json
Normal file
6
mobile/ios/Runner/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
305
mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift
Normal file
305
mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
//
|
||||||
|
// BackgroundServicePlugin.swift
|
||||||
|
// Runner
|
||||||
|
//
|
||||||
|
// Created by Marty Fuhry on 2/14/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Flutter
|
||||||
|
import BackgroundTasks
|
||||||
|
import path_provider_foundation
|
||||||
|
|
||||||
|
class BackgroundServicePlugin: NSObject, FlutterPlugin {
|
||||||
|
|
||||||
|
public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
|
||||||
|
|
||||||
|
public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) {
|
||||||
|
flutterPluginRegistrantCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause the application in XCode, then enter
|
||||||
|
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"]
|
||||||
|
// or
|
||||||
|
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"]
|
||||||
|
// Then resume the application see the background code run
|
||||||
|
// Tested on a physical device, not a simulator
|
||||||
|
// This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing.
|
||||||
|
// In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command
|
||||||
|
|
||||||
|
// This is the task ID in Info.plist to register as our background task ID
|
||||||
|
public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch"
|
||||||
|
public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing"
|
||||||
|
|
||||||
|
// Establish communication with the main isolate and set up the channel call
|
||||||
|
// to this BackgroundServicePlugion()
|
||||||
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
let channel = FlutterMethodChannel(
|
||||||
|
name: "immich/foregroundChannel",
|
||||||
|
binaryMessenger: registrar.messenger()
|
||||||
|
)
|
||||||
|
|
||||||
|
let instance = BackgroundServicePlugin()
|
||||||
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||||
|
registrar.addApplicationDelegate(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers the Flutter engine with the plugins, used by the other Background Flutter engine
|
||||||
|
public static func register(engine: FlutterEngine) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers the task IDs from the system so that we can process them here in this class
|
||||||
|
public static func registerBackgroundProcessing() {
|
||||||
|
|
||||||
|
let processingRegisterd = BGTaskScheduler.shared.register(
|
||||||
|
forTaskWithIdentifier: backgroundProcessingTaskID,
|
||||||
|
using: nil) { task in
|
||||||
|
if task is BGProcessingTask {
|
||||||
|
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchRegisterd = BGTaskScheduler.shared.register(
|
||||||
|
forTaskWithIdentifier: backgroundFetchTaskID,
|
||||||
|
using: nil) { task in
|
||||||
|
if task is BGAppRefreshTask {
|
||||||
|
handleBackgroundFetch(task: task as! BGAppRefreshTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the channel methods from Flutter
|
||||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
switch call.method {
|
||||||
|
case "enable":
|
||||||
|
handleBackgroundEnable(call: call, result: result)
|
||||||
|
break
|
||||||
|
case "configure":
|
||||||
|
handleConfigure(call: call, result: result)
|
||||||
|
break
|
||||||
|
case "disable":
|
||||||
|
handleDisable(call: call, result: result)
|
||||||
|
break
|
||||||
|
case "isEnabled":
|
||||||
|
handleIsEnabled(call: call, result: result)
|
||||||
|
break
|
||||||
|
case "isIgnoringBatteryOptimizations":
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
break
|
||||||
|
case "lastBackgroundFetchTime":
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time")
|
||||||
|
result(lastRunTime)
|
||||||
|
case "lastBackgroundProcessingTime":
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let lastRunTime = defaults.value(forKey: "last_background_processing_run_time")
|
||||||
|
result(lastRunTime)
|
||||||
|
case "numberOfBackgroundProcesses":
|
||||||
|
handleNumberOfProcesses(call: call, result: result)
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the flutter code when enabled so that we can turn on the backround services
|
||||||
|
// and save the callback information to communicate on this method channel
|
||||||
|
public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
|
|
||||||
|
// Needs to parse the arguments from the method call
|
||||||
|
guard let args = call.arguments as? Array<Any> else {
|
||||||
|
print("Cannot parse args as array: \(call.arguments)")
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requires 3 arguments in the array
|
||||||
|
guard args.count == 3 else {
|
||||||
|
print("Requires 3 arguments and received \(args.count)")
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses the arguments
|
||||||
|
let callbackHandle = args[0] as? Int64
|
||||||
|
let notificationTitle = args[1] as? String
|
||||||
|
let instant = args[2] as? Bool
|
||||||
|
|
||||||
|
// Write enabled to settings
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
// We are now enabled, so store this
|
||||||
|
defaults.set(true, forKey: "background_service_enabled")
|
||||||
|
|
||||||
|
// The callback handle is an int64 address to communicate with the main isolate's
|
||||||
|
// entry function
|
||||||
|
defaults.set(callbackHandle, forKey: "callback_handle")
|
||||||
|
|
||||||
|
// This is not used yet and will need to be implemented
|
||||||
|
defaults.set(notificationTitle, forKey: "notification_title")
|
||||||
|
|
||||||
|
// Schedule the background services if instant
|
||||||
|
if (instant ?? true) {
|
||||||
|
BackgroundServicePlugin.scheduleBackgroundSync()
|
||||||
|
BackgroundServicePlugin.scheduleBackgroundFetch()
|
||||||
|
}
|
||||||
|
result(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the flutter code at launch to see if the background service is enabled or not
|
||||||
|
func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let enabled = defaults.value(forKey: "background_service_enabled") as? Bool
|
||||||
|
|
||||||
|
// False by default
|
||||||
|
result(enabled ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by the Flutter code whenever a change in configuration is set
|
||||||
|
func handleConfigure(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
|
|
||||||
|
// Needs to be able to parse the arguments or else fail
|
||||||
|
guard let args = call.arguments as? Array<Any> else {
|
||||||
|
print("Cannot parse args as array: \(call.arguments)")
|
||||||
|
result(FlutterError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needs to have 4 arguments in the call or else fail
|
||||||
|
guard args.count == 4 else {
|
||||||
|
print("Not enough arguments, 4 required: \(args.count) given")
|
||||||
|
result(FlutterError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the arguments from the method call
|
||||||
|
let requireUnmeteredNetwork = args[0] as? Bool
|
||||||
|
let requireCharging = args[1] as? Bool
|
||||||
|
let triggerUpdateDelay = args[2] as? Int
|
||||||
|
let triggerMaxDelay = args[3] as? Int
|
||||||
|
|
||||||
|
// Store the values from the call in the defaults
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network")
|
||||||
|
defaults.set(requireCharging, forKey: "require_charging")
|
||||||
|
defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay")
|
||||||
|
defaults.set(triggerMaxDelay, forKey: "trigger_max_delay")
|
||||||
|
|
||||||
|
// Cancel the background services and reschedule them
|
||||||
|
BGTaskScheduler.shared.cancelAllTaskRequests()
|
||||||
|
BackgroundServicePlugin.scheduleBackgroundSync()
|
||||||
|
BackgroundServicePlugin.scheduleBackgroundFetch()
|
||||||
|
result(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the number of currently scheduled background processes to Flutter, striclty
|
||||||
|
// for debugging
|
||||||
|
func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
BGTaskScheduler.shared.getPendingTaskRequests { requests in
|
||||||
|
result(requests.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disables the service, cancels all the task requests
|
||||||
|
func handleDisable(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
defaults.set(false, forKey: "background_service_enabled")
|
||||||
|
|
||||||
|
BGTaskScheduler.shared.cancelAllTaskRequests()
|
||||||
|
result(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedules a short-running background sync to sync only a few photos
|
||||||
|
static func scheduleBackgroundFetch() {
|
||||||
|
// We will only schedule this task to run if the user has explicitely allowed us to backup while
|
||||||
|
// not connected to power
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
if defaults.value(forKey: "require_charging") as? Bool == true {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID)
|
||||||
|
|
||||||
|
// Use 5 minutes from now as earliest begin date
|
||||||
|
backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try BGTaskScheduler.shared.submit(backgroundFetch)
|
||||||
|
} catch {
|
||||||
|
print("Could not schedule the background task \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedules a long-running background sync for syncing all of the photos
|
||||||
|
static func scheduleBackgroundSync() {
|
||||||
|
let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID)
|
||||||
|
|
||||||
|
// We need the values for requiring charging
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
let requireCharging = defaults.value(forKey: "require_charging") as? Bool
|
||||||
|
|
||||||
|
// Always require network connectivity, and set the require charging from the above
|
||||||
|
backgroundProcessing.requiresNetworkConnectivity = true
|
||||||
|
backgroundProcessing.requiresExternalPower = requireCharging ?? true
|
||||||
|
|
||||||
|
// Use 15 minutes from now as earliest begin date
|
||||||
|
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Submit the task to the scheduler
|
||||||
|
try BGTaskScheduler.shared.submit(backgroundProcessing)
|
||||||
|
} catch {
|
||||||
|
print("Could not schedule the background task \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler
|
||||||
|
static func handleBackgroundFetch(task: BGAppRefreshTask) {
|
||||||
|
// Log the time of last background processing to now
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time")
|
||||||
|
|
||||||
|
// Schedule the next sync task so we can run this again later
|
||||||
|
scheduleBackgroundFetch()
|
||||||
|
|
||||||
|
// The background sync task should only run for 20 seconds at most
|
||||||
|
BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler
|
||||||
|
static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||||
|
// Log the time of last background processing to now
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time")
|
||||||
|
|
||||||
|
// Schedule the next sync task so we run this again later
|
||||||
|
scheduleBackgroundSync()
|
||||||
|
|
||||||
|
// We won't specify a max time for the background sync service, so this can run for longer
|
||||||
|
BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a synchronous function which uses a semaphore to run the background sync worker's run
|
||||||
|
// function, which will create a background Isolate and communicate with the Flutter code to back
|
||||||
|
// up the assets. When it completes, we signal the semaphore and complete the execution allowing the
|
||||||
|
// control to pass back to the caller synchronously
|
||||||
|
static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) {
|
||||||
|
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let backgroundWorker = BackgroundSyncWorker { _ in
|
||||||
|
semaphore.signal()
|
||||||
|
}
|
||||||
|
task.expirationHandler = {
|
||||||
|
backgroundWorker.cancel()
|
||||||
|
task.setTaskCompleted(success: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundWorker.run(maxSeconds: maxSeconds)
|
||||||
|
task.setTaskCompleted(success: true)
|
||||||
|
}
|
||||||
|
semaphore.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
188
mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
Normal file
188
mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
//
|
||||||
|
// BackgroundSyncProcessing.swift
|
||||||
|
// Runner
|
||||||
|
//
|
||||||
|
// Created by Marty Fuhry on 2/6/23.
|
||||||
|
//
|
||||||
|
// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Flutter
|
||||||
|
import BackgroundTasks
|
||||||
|
|
||||||
|
// The background worker which creates a new Flutter VM, communicates with it
|
||||||
|
// to run the backup job, and then finishes execution and calls back to its callback
|
||||||
|
// handler
|
||||||
|
class BackgroundSyncWorker {
|
||||||
|
|
||||||
|
// The Flutter engine we create for background execution.
|
||||||
|
// This is not the main Flutter engine which shows the UI,
|
||||||
|
// this is a brand new isolate created and managed in this code
|
||||||
|
// here. It does not share memory with the main
|
||||||
|
// Flutter engine which shows the UI.
|
||||||
|
// It needs to be started up, registered, and torn down here
|
||||||
|
let engine: FlutterEngine? = FlutterEngine(
|
||||||
|
name: "BackgroundImmich"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The background message passing channel
|
||||||
|
var channel: FlutterMethodChannel?
|
||||||
|
|
||||||
|
var completionHandler: (UIBackgroundFetchResult) -> Void
|
||||||
|
let taskSessionStart = Date()
|
||||||
|
|
||||||
|
// We need the completion handler to tell the system when we are done running
|
||||||
|
init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||||
|
|
||||||
|
// This is the background message passing channel to be used with the background engine
|
||||||
|
// created here in this platform code
|
||||||
|
self.channel = FlutterMethodChannel(
|
||||||
|
name: "immich/backgroundChannel",
|
||||||
|
binaryMessenger: engine!.binaryMessenger
|
||||||
|
)
|
||||||
|
self.completionHandler = completionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles all of the messages from the Flutter VM called into this platform code
|
||||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
switch call.method {
|
||||||
|
case "initialized":
|
||||||
|
// Initialize tells us that we can now call into the Flutter VM to tell it to begin the update
|
||||||
|
self.channel?.invokeMethod(
|
||||||
|
"backgroundProcessing",
|
||||||
|
arguments: nil,
|
||||||
|
result: { flutterResult in
|
||||||
|
|
||||||
|
// This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or
|
||||||
|
// if this execution failed
|
||||||
|
let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed
|
||||||
|
|
||||||
|
// Show the task duration
|
||||||
|
let taskSessionCompleter = Date()
|
||||||
|
let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart)
|
||||||
|
print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)")
|
||||||
|
|
||||||
|
// Complete the execution
|
||||||
|
self.complete(result)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "updateNotification":
|
||||||
|
// TODO: implement update notification
|
||||||
|
result(true)
|
||||||
|
break
|
||||||
|
case "showError":
|
||||||
|
// TODO: implement show error
|
||||||
|
result(true)
|
||||||
|
break
|
||||||
|
case "clearErrorNotifications":
|
||||||
|
// TODO: implement clear error notifications
|
||||||
|
result(true)
|
||||||
|
break
|
||||||
|
case "hasContentChanged":
|
||||||
|
// This is only called for Android, but we provide an implementation here
|
||||||
|
// telling Flutter that we don't have any information about whether the gallery
|
||||||
|
// contents have changed or not, so we can just say "no, they've not changed"
|
||||||
|
result(false)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result(FlutterError())
|
||||||
|
self.complete(UIBackgroundFetchResult.failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the background sync by starting up a new isolate and handling the calls
|
||||||
|
// until it completes
|
||||||
|
public func run(maxSeconds: Int?) {
|
||||||
|
// We need the callback handle to start up the Flutter VM from the entry point
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else {
|
||||||
|
// Can't find the callback handle, this is fatal
|
||||||
|
complete(UIBackgroundFetchResult.failed)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the provided callbackHandle to get the callback function
|
||||||
|
guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
|
||||||
|
// We need this callback or else this is fatal
|
||||||
|
complete(UIBackgroundFetchResult.failed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check for the engine existing
|
||||||
|
if engine == nil {
|
||||||
|
complete(UIBackgroundFetchResult.failed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the engine
|
||||||
|
let isRunning = engine!.run(
|
||||||
|
withEntrypoint: callback.callbackName,
|
||||||
|
libraryURI: callback.callbackLibraryPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// If this engine isn't running, this is fatal
|
||||||
|
if !isRunning {
|
||||||
|
complete(UIBackgroundFetchResult.failed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a timer, we need to start the timer to cancel ourselves
|
||||||
|
// so that we don't run longer than the provided maxSeconds
|
||||||
|
// After maxSeconds has elapsed, we will invoke "systemStop"
|
||||||
|
if maxSeconds != nil {
|
||||||
|
// Schedule a non-repeating timer to run after maxSeconds
|
||||||
|
let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!),
|
||||||
|
repeats: false) { timer in
|
||||||
|
// The callback invalidates the timer and stops execution
|
||||||
|
timer.invalidate()
|
||||||
|
|
||||||
|
// If the channel is already deallocated, we don't need to do anything
|
||||||
|
if self.channel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the Flutter VM to stop backing up now
|
||||||
|
self.channel?.invokeMethod(
|
||||||
|
"systemStop",
|
||||||
|
arguments: nil,
|
||||||
|
result: nil)
|
||||||
|
|
||||||
|
// Complete the execution
|
||||||
|
self.complete(UIBackgroundFetchResult.newData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the handle function to the channel message handler
|
||||||
|
self.channel?.setMethodCallHandler(handle)
|
||||||
|
|
||||||
|
// Register this to get access to the plugins on the platform channel
|
||||||
|
BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancels execution of this task, used by the system's task expiration handler
|
||||||
|
// which is called shortly before execution is about to expire
|
||||||
|
public func cancel() {
|
||||||
|
// If the channel is already deallocated, we don't need to do anything
|
||||||
|
if self.channel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the Flutter VM to stop backing up now
|
||||||
|
self.channel?.invokeMethod(
|
||||||
|
"systemStop",
|
||||||
|
arguments: nil,
|
||||||
|
result: nil)
|
||||||
|
|
||||||
|
// Complete the execution
|
||||||
|
self.complete(UIBackgroundFetchResult.newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completes the execution, destroys the engine, and sends a completion to our callback completionHandler
|
||||||
|
private func complete(_ fetchResult: UIBackgroundFetchResult) {
|
||||||
|
engine?.destroyContext()
|
||||||
|
channel = nil
|
||||||
|
completionHandler(fetchResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,13 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>app.alextran.immich.backgroundFetch</string>
|
||||||
|
<string>app.alextran.immich.backgroundProcessing</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@ -12,66 +19,6 @@
|
|||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>immich_mobile</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.47.0</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>86</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
|
||||||
<string>Enable location setting to show position of assets on map</string>
|
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>Enable location setting to show position of assets on map</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>We need to manage backup your photos album</string>
|
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
||||||
<string>We need to manage backup your photos album</string>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<true/>
|
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
|
||||||
<true/>
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>https</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>cs</string>
|
<string>cs</string>
|
||||||
@ -92,11 +39,74 @@
|
|||||||
<string>sk</string>
|
<string>sk</string>
|
||||||
<string>zh</string>
|
<string>zh</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>CFBundleName</key>
|
||||||
|
<string>immich_mobile</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.47.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>86</string>
|
||||||
|
<key>FLTEnableImpeller</key>
|
||||||
|
<true/>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>https</string>
|
||||||
|
</array>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
|
<string>Enable location setting to show position of assets on map</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>Enable location setting to show position of assets on map</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>We need to manage backup your photos album</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>processing</string>
|
||||||
|
</array>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<true/>
|
||||||
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -47,10 +47,6 @@ Future<void> openBoxes() async {
|
|||||||
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox),
|
||||||
Hive.openBox(hiveGithubReleaseInfoBox),
|
Hive.openBox(hiveGithubReleaseInfoBox),
|
||||||
Hive.openBox(userSettingInfoBox),
|
Hive.openBox(userSettingInfoBox),
|
||||||
if (!Platform.isAndroid) Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
|
||||||
if (!Platform.isAndroid)
|
|
||||||
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
|
||||||
if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox),
|
|
||||||
EasyLocalization.ensureInitialized(),
|
EasyLocalization.ensureInitialized(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:ui' show IsolateNameServer, PluginUtilities;
|
import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -10,7 +10,6 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
|
|
||||||
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
@ -19,6 +18,7 @@ import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
|||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
|
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
final backgroundServiceProvider = Provider(
|
final backgroundServiceProvider = Provider(
|
||||||
@ -51,8 +51,7 @@ class BackgroundService {
|
|||||||
late final _Throttle _throttledDetailNotify =
|
late final _Throttle _throttledDetailNotify =
|
||||||
_Throttle(_updateDetailProgress, notifyInterval);
|
_Throttle(_updateDetailProgress, notifyInterval);
|
||||||
Completer<bool> _hasAccessCompleter = Completer();
|
Completer<bool> _hasAccessCompleter = Completer();
|
||||||
late Future<bool> _hasAccess =
|
late Future<bool> _hasAccess = _hasAccessCompleter.future;
|
||||||
Platform.isAndroid ? _hasAccessCompleter.future : Future.value(true);
|
|
||||||
|
|
||||||
Future<bool> get hasAccess => _hasAccess;
|
Future<bool> get hasAccess => _hasAccess;
|
||||||
|
|
||||||
@ -67,9 +66,6 @@ class BackgroundService {
|
|||||||
|
|
||||||
/// Enqueues the background service
|
/// Enqueues the background service
|
||||||
Future<bool> enableService({bool immediate = false}) async {
|
Future<bool> enableService({bool immediate = false}) async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
|
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
|
||||||
final String title =
|
final String title =
|
||||||
@ -89,9 +85,6 @@ class BackgroundService {
|
|||||||
int triggerUpdateDelay = 5000,
|
int triggerUpdateDelay = 5000,
|
||||||
int triggerMaxDelay = 50000,
|
int triggerMaxDelay = 50000,
|
||||||
}) async {
|
}) async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
final bool ok = await _foregroundChannel.invokeMethod(
|
final bool ok = await _foregroundChannel.invokeMethod(
|
||||||
'configure',
|
'configure',
|
||||||
@ -110,9 +103,6 @@ class BackgroundService {
|
|||||||
|
|
||||||
/// Cancels the background service (if currently running) and removes it from work queue
|
/// Cancels the background service (if currently running) and removes it from work queue
|
||||||
Future<bool> disableService() async {
|
Future<bool> disableService() async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
final ok = await _foregroundChannel.invokeMethod('disable');
|
final ok = await _foregroundChannel.invokeMethod('disable');
|
||||||
return ok;
|
return ok;
|
||||||
@ -123,9 +113,6 @@ class BackgroundService {
|
|||||||
|
|
||||||
/// Returns `true` if the background service is enabled
|
/// Returns `true` if the background service is enabled
|
||||||
Future<bool> isBackgroundBackupEnabled() async {
|
Future<bool> isBackgroundBackupEnabled() async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return await _foregroundChannel.invokeMethod("isEnabled");
|
return await _foregroundChannel.invokeMethod("isEnabled");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -135,7 +122,8 @@ class BackgroundService {
|
|||||||
|
|
||||||
/// Returns `true` if battery optimizations are disabled
|
/// Returns `true` if battery optimizations are disabled
|
||||||
Future<bool> isIgnoringBatteryOptimizations() async {
|
Future<bool> isIgnoringBatteryOptimizations() async {
|
||||||
if (!Platform.isAndroid) {
|
// iOS does not need battery optimizations enabled
|
||||||
|
if (Platform.isIOS) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -156,9 +144,6 @@ class BackgroundService {
|
|||||||
bool isDetail = false,
|
bool isDetail = false,
|
||||||
bool onlyIfFG = false,
|
bool onlyIfFG = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (_isBackgroundInitialized) {
|
if (_isBackgroundInitialized) {
|
||||||
return _backgroundChannel.invokeMethod<bool>(
|
return _backgroundChannel.invokeMethod<bool>(
|
||||||
@ -178,9 +163,6 @@ class BackgroundService {
|
|||||||
String? content,
|
String? content,
|
||||||
String? individualTag,
|
String? individualTag,
|
||||||
}) async {
|
}) async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
|
if (_isBackgroundInitialized && _errorGracePeriodExceeded) {
|
||||||
return await _backgroundChannel
|
return await _backgroundChannel
|
||||||
@ -193,9 +175,6 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _clearErrorNotifications() async {
|
Future<bool> _clearErrorNotifications() async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (_isBackgroundInitialized) {
|
if (_isBackgroundInitialized) {
|
||||||
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
|
return await _backgroundChannel.invokeMethod('clearErrorNotifications');
|
||||||
@ -210,9 +189,6 @@ class BackgroundService {
|
|||||||
|
|
||||||
/// await to ensure this thread (foreground or background) has exclusive access
|
/// await to ensure this thread (foreground or background) has exclusive access
|
||||||
Future<bool> acquireLock() async {
|
Future<bool> acquireLock() async {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (_hasLock) {
|
if (_hasLock) {
|
||||||
debugPrint("WARNING: [acquireLock] called more than once");
|
debugPrint("WARNING: [acquireLock] called more than once");
|
||||||
return true;
|
return true;
|
||||||
@ -253,7 +229,7 @@ class BackgroundService {
|
|||||||
while (_wantsLockTime == lockTime) {
|
while (_wantsLockTime == lockTime) {
|
||||||
other.send(tempSp);
|
other.send(tempSp);
|
||||||
final dynamic answer = await bs.first
|
final dynamic answer = await bs.first
|
||||||
.timeout(const Duration(seconds: 5), onTimeout: () => null);
|
.timeout(const Duration(seconds: 3), onTimeout: () => null);
|
||||||
if (_wantsLockTime != lockTime) {
|
if (_wantsLockTime != lockTime) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -270,7 +246,7 @@ class BackgroundService {
|
|||||||
// other isolate is still active
|
// other isolate is still active
|
||||||
}
|
}
|
||||||
final dynamic isFinished = await bs.first
|
final dynamic isFinished = await bs.first
|
||||||
.timeout(const Duration(seconds: 5), onTimeout: () => false);
|
.timeout(const Duration(seconds: 3), onTimeout: () => false);
|
||||||
if (isFinished == true) {
|
if (isFinished == true) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -288,9 +264,6 @@ class BackgroundService {
|
|||||||
|
|
||||||
/// releases the exclusive access lock
|
/// releases the exclusive access lock
|
||||||
void releaseLock() {
|
void releaseLock() {
|
||||||
if (!Platform.isAndroid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_wantsLockTime = 0;
|
_wantsLockTime = 0;
|
||||||
if (_hasLock) {
|
if (_hasLock) {
|
||||||
_hasAccessCompleter = Completer();
|
_hasAccessCompleter = Completer();
|
||||||
@ -311,17 +284,35 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _callHandler(MethodCall call) async {
|
Future<bool> _callHandler(MethodCall call) async {
|
||||||
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
// NOTE: I'm not sure this is strictly necessary anymore, but
|
||||||
|
// out of an abundance of caution, we will keep it in until someone
|
||||||
|
// can say for sure
|
||||||
|
PathProviderIOS.registerWith();
|
||||||
|
}
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
|
case "backgroundProcessing":
|
||||||
case "onAssetsChanged":
|
case "onAssetsChanged":
|
||||||
final Future<bool> translationsLoaded = loadTranslations();
|
|
||||||
try {
|
try {
|
||||||
_clearErrorNotifications();
|
_clearErrorNotifications();
|
||||||
final bool hasAccess = await acquireLock();
|
|
||||||
|
// iOS should time out after some threshhold so it doesn't wait
|
||||||
|
// indefinitely and can run later
|
||||||
|
// Android is fine to wait here until the lock releases
|
||||||
|
final waitForLock = Platform.isIOS
|
||||||
|
? acquireLock()
|
||||||
|
.timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => false,
|
||||||
|
)
|
||||||
|
: acquireLock();
|
||||||
|
|
||||||
|
final bool hasAccess = await waitForLock;
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
debugPrint("[_callHandler] could not acquire lock, exiting");
|
debugPrint("[_callHandler] could not acquire lock, exiting");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await translationsLoaded;
|
|
||||||
final bool ok = await _onAssetsChanged();
|
final bool ok = await _onAssetsChanged();
|
||||||
return ok;
|
return ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -388,9 +379,9 @@ class BackgroundService {
|
|||||||
.put(backupFailedSince, DateTime.now());
|
.put(backupFailedSince, DateTime.now());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// check for new assets added while performing backup
|
// Android should check for new assets added while performing backup
|
||||||
} while (true ==
|
} while (Platform.isAndroid &&
|
||||||
await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
|
true == await _backgroundChannel.invokeMethod<bool>("hasContentChanged"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,6 +551,28 @@ class BackgroundService {
|
|||||||
final String total = numberFormat.format(bytesTotal / 1024.0);
|
final String total = numberFormat.format(bytesTotal / 1024.0);
|
||||||
return "$percent% ($done/$total$unit)";
|
return "$percent% ($done/$total$unit)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<DateTime?> getIOSBackupLastRun(IosBackgroundTask task) async {
|
||||||
|
// Seconds since last run
|
||||||
|
final double? lastRun = task == IosBackgroundTask.fetch
|
||||||
|
? await _foregroundChannel.invokeMethod('lastBackgroundFetchTime')
|
||||||
|
: await _foregroundChannel.invokeMethod('lastBackgroundProcessingTime');
|
||||||
|
if (lastRun == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final time = DateTime.fromMillisecondsSinceEpoch(lastRun.toInt() * 1000);
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getIOSBackupNumberOfProcesses() async {
|
||||||
|
return await _foregroundChannel
|
||||||
|
.invokeMethod('numberOfBackgroundProcesses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IosBackgroundTask {
|
||||||
|
fetch,
|
||||||
|
processing
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Throttle {
|
class _Throttle {
|
||||||
@ -603,6 +616,7 @@ class _Throttle {
|
|||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void _nativeEntry() {
|
void _nativeEntry() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
DartPluginRegistrant.ensureInitialized();
|
||||||
BackgroundService backgroundService = BackgroundService();
|
BackgroundService backgroundService = BackgroundService();
|
||||||
backgroundService._setupBackgroundCallHandler();
|
backgroundService._setupBackgroundCallHandler();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@ -130,7 +128,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
requireCharging != null ||
|
requireCharging != null ||
|
||||||
triggerDelay != null,
|
triggerDelay != null,
|
||||||
);
|
);
|
||||||
if (Platform.isAndroid) {
|
|
||||||
final bool wasEnabled = state.backgroundBackup;
|
final bool wasEnabled = state.backgroundBackup;
|
||||||
final bool wasWifi = state.backupRequireWifi;
|
final bool wasWifi = state.backupRequireWifi;
|
||||||
final bool wasCharging = state.backupRequireCharging;
|
final bool wasCharging = state.backupRequireCharging;
|
||||||
@ -180,7 +177,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
onError("backup_controller_page_background_configure_error");
|
onError("backup_controller_page_background_configure_error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -577,48 +573,47 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resumeBackup() async {
|
Future<void> resumeBackup() async {
|
||||||
if (Platform.isAndroid) {
|
|
||||||
// assumes the background service is currently running
|
// assumes the background service is currently running
|
||||||
// if true, waits until it has stopped to update the app state from HiveDB
|
// if true, waits until it has stopped to update the app state from HiveDB
|
||||||
// before actually resuming backup by calling the internal `_resumeBackup`
|
// before actually resuming backup by calling the internal `_resumeBackup`
|
||||||
final BackUpProgressEnum previous = state.backupProgress;
|
final BackUpProgressEnum previous = state.backupProgress;
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
|
||||||
final bool hasLock = await _backgroundService.acquireLock();
|
final bool hasLock = await _backgroundService.acquireLock();
|
||||||
if (!hasLock) {
|
if (!hasLock) {
|
||||||
log.warning("WARNING [resumeBackup] failed to acquireLock");
|
log.warning("WARNING [resumeBackup] failed to acquireLock");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
|
||||||
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
|
||||||
Hive.openBox(backgroundBackupInfoBox),
|
Hive.openBox(backgroundBackupInfoBox),
|
||||||
]);
|
]);
|
||||||
final HiveBackupAlbums? albums =
|
final HiveBackupAlbums? albums =
|
||||||
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
|
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
|
||||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||||
if (albums != null) {
|
if (albums != null) {
|
||||||
selectedAlbums = _updateAlbumsBackupTime(
|
selectedAlbums = _updateAlbumsBackupTime(
|
||||||
selectedAlbums,
|
selectedAlbums,
|
||||||
albums.selectedAlbumIds,
|
albums.selectedAlbumIds,
|
||||||
albums.lastSelectedBackupTime,
|
albums.lastSelectedBackupTime,
|
||||||
);
|
);
|
||||||
excludedAlbums = _updateAlbumsBackupTime(
|
excludedAlbums = _updateAlbumsBackupTime(
|
||||||
excludedAlbums,
|
excludedAlbums,
|
||||||
albums.excludedAlbumsIds,
|
albums.excludedAlbumsIds,
|
||||||
albums.lastExcludedBackupTime,
|
albums.lastExcludedBackupTime,
|
||||||
);
|
|
||||||
}
|
|
||||||
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
|
|
||||||
state = state.copyWith(
|
|
||||||
backupProgress: previous,
|
|
||||||
selectedBackupAlbums: selectedAlbums,
|
|
||||||
excludedBackupAlbums: excludedAlbums,
|
|
||||||
backupRequireWifi: backgroundBox.get(backupRequireWifi),
|
|
||||||
backupRequireCharging: backgroundBox.get(backupRequireCharging),
|
|
||||||
backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
|
||||||
|
state = state.copyWith(
|
||||||
|
backupProgress: previous,
|
||||||
|
selectedBackupAlbums: selectedAlbums,
|
||||||
|
excludedBackupAlbums: excludedAlbums,
|
||||||
|
backupRequireWifi: backgroundBox.get(backupRequireWifi),
|
||||||
|
backupRequireCharging: backgroundBox.get(backupRequireCharging),
|
||||||
|
backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
|
||||||
|
);
|
||||||
return _resumeBackup();
|
return _resumeBackup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,8 +644,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
AppStateEnum.paused,
|
AppStateEnum.paused,
|
||||||
AppStateEnum.detached,
|
AppStateEnum.detached,
|
||||||
];
|
];
|
||||||
if (Platform.isAndroid &&
|
if (allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
|
||||||
allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
|
|
||||||
try {
|
try {
|
||||||
if (Hive.isBoxOpen(hiveBackupInfoBox)) {
|
if (Hive.isBoxOpen(hiveBackupInfoBox)) {
|
||||||
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
|
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
|
||||||
@ -683,6 +677,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
_backgroundService.releaseLock();
|
_backgroundService.releaseLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupProvider =
|
final backupProvider =
|
||||||
|
67
mobile/lib/modules/backup/ui/ios_debug_info_tile.dart
Normal file
67
mobile/lib/modules/backup/ui/ios_debug_info_tile.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// This is a simple debug widget which should be removed later on when we are
|
||||||
|
/// more confident about background sync
|
||||||
|
class IosDebugInfoTile extends HookConsumerWidget {
|
||||||
|
const IosDebugInfoTile({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final futures = [
|
||||||
|
ref.read(backgroundServiceProvider)
|
||||||
|
.getIOSBackupLastRun(IosBackgroundTask.fetch),
|
||||||
|
ref.read(backgroundServiceProvider)
|
||||||
|
.getIOSBackupLastRun(IosBackgroundTask.processing),
|
||||||
|
ref.read(backgroundServiceProvider)
|
||||||
|
.getIOSBackupNumberOfProcesses(),
|
||||||
|
];
|
||||||
|
return FutureBuilder<List<dynamic>>(
|
||||||
|
future: Future.wait(futures),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
String? title;
|
||||||
|
String? subtitle;
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final results = snapshot.data as List<dynamic>;
|
||||||
|
final fetch = results[0] as DateTime?;
|
||||||
|
final processing = results[1] as DateTime?;
|
||||||
|
final processes = results[2] as int;
|
||||||
|
|
||||||
|
final processOrProcesses = processes == 1 ? 'process' : 'processes';
|
||||||
|
final numberOrZero = processes == 0 ? 'No' : processes.toString();
|
||||||
|
title = '$numberOrZero background $processOrProcesses queued';
|
||||||
|
|
||||||
|
final df = DateFormat.yMd().add_jm();
|
||||||
|
if (fetch == null && processing == null) {
|
||||||
|
subtitle = 'No background sync job has run yet';
|
||||||
|
} else if (fetch != null && processing == null) {
|
||||||
|
subtitle = 'Fetch ran ${df.format(fetch)}';
|
||||||
|
} else if (processing != null && fetch == null) {
|
||||||
|
subtitle = 'Processing ran ${df.format(processing)}';
|
||||||
|
} else {
|
||||||
|
final fetchOrProcessing = fetch!.isAfter(processing!)
|
||||||
|
? fetch
|
||||||
|
: processing;
|
||||||
|
subtitle = 'Last sync ${df.format(fetchOrProcessing)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: ListTile(
|
||||||
|
key: ValueKey(title),
|
||||||
|
title: Text(title ?? ''),
|
||||||
|
subtitle: Text(subtitle ?? ''),
|
||||||
|
leading: Icon(
|
||||||
|
Icons.bug_report,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
|
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@ -195,7 +196,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ListTile buildBackgroundBackupController() {
|
Widget buildBackgroundBackupController() {
|
||||||
final bool isBackgroundEnabled = backupState.backgroundBackup;
|
final bool isBackgroundEnabled = backupState.backgroundBackup;
|
||||||
final bool isWifiRequired = backupState.backupRequireWifi;
|
final bool isWifiRequired = backupState.backupRequireWifi;
|
||||||
final bool isChargingRequired = backupState.backupRequireCharging;
|
final bool isChargingRequired = backupState.backupRequireCharging;
|
||||||
@ -240,118 +241,126 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
final triggerDelay =
|
final triggerDelay =
|
||||||
useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
|
useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
|
||||||
|
|
||||||
return ListTile(
|
return Column(
|
||||||
isThreeLine: true,
|
children: [
|
||||||
leading: isBackgroundEnabled
|
ListTile(
|
||||||
? Icon(
|
isThreeLine: true,
|
||||||
Icons.cloud_sync_rounded,
|
leading: isBackgroundEnabled
|
||||||
color: activeColor,
|
? Icon(
|
||||||
)
|
Icons.cloud_sync_rounded,
|
||||||
: const Icon(Icons.cloud_sync_rounded),
|
color: activeColor,
|
||||||
title: Text(
|
)
|
||||||
isBackgroundEnabled
|
: const Icon(Icons.cloud_sync_rounded),
|
||||||
? "backup_controller_page_background_is_on"
|
title: Text(
|
||||||
: "backup_controller_page_background_is_off",
|
isBackgroundEnabled
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
? "backup_controller_page_background_is_on"
|
||||||
).tr(),
|
: "backup_controller_page_background_is_off",
|
||||||
subtitle: Column(
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
).tr(),
|
||||||
children: [
|
subtitle: Column(
|
||||||
if (!isBackgroundEnabled)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
if (!isBackgroundEnabled)
|
||||||
child:
|
Padding(
|
||||||
const Text("backup_controller_page_background_description")
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
.tr(),
|
child:
|
||||||
),
|
const Text("backup_controller_page_background_description")
|
||||||
if (isBackgroundEnabled)
|
.tr(),
|
||||||
SwitchListTile(
|
|
||||||
title:
|
|
||||||
const Text("backup_controller_page_background_wifi").tr(),
|
|
||||||
secondary: Icon(
|
|
||||||
Icons.wifi,
|
|
||||||
color: isWifiRequired ? activeColor : null,
|
|
||||||
),
|
|
||||||
dense: true,
|
|
||||||
activeColor: activeColor,
|
|
||||||
value: isWifiRequired,
|
|
||||||
onChanged: hasExclusiveAccess
|
|
||||||
? (isChecked) => ref
|
|
||||||
.read(backupProvider.notifier)
|
|
||||||
.configureBackgroundBackup(
|
|
||||||
requireWifi: isChecked,
|
|
||||||
onError: showErrorToUser,
|
|
||||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
if (isBackgroundEnabled)
|
|
||||||
SwitchListTile(
|
|
||||||
title: const Text("backup_controller_page_background_charging")
|
|
||||||
.tr(),
|
|
||||||
secondary: Icon(
|
|
||||||
Icons.charging_station,
|
|
||||||
color: isChargingRequired ? activeColor : null,
|
|
||||||
),
|
|
||||||
dense: true,
|
|
||||||
activeColor: activeColor,
|
|
||||||
value: isChargingRequired,
|
|
||||||
onChanged: hasExclusiveAccess
|
|
||||||
? (isChecked) => ref
|
|
||||||
.read(backupProvider.notifier)
|
|
||||||
.configureBackgroundBackup(
|
|
||||||
requireCharging: isChecked,
|
|
||||||
onError: showErrorToUser,
|
|
||||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
if (isBackgroundEnabled)
|
|
||||||
ListTile(
|
|
||||||
isThreeLine: false,
|
|
||||||
dense: true,
|
|
||||||
enabled: hasExclusiveAccess,
|
|
||||||
title: const Text(
|
|
||||||
'backup_controller_page_background_delay',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
|
if (isBackgroundEnabled && Platform.isAndroid)
|
||||||
subtitle: Slider(
|
SwitchListTile(
|
||||||
value: triggerDelay.value,
|
title:
|
||||||
onChanged: hasExclusiveAccess
|
const Text("backup_controller_page_background_wifi").tr(),
|
||||||
? (double v) => triggerDelay.value = v
|
secondary: Icon(
|
||||||
: null,
|
Icons.wifi,
|
||||||
onChangeEnd: (double v) => ref
|
color: isWifiRequired ? activeColor : null,
|
||||||
.read(backupProvider.notifier)
|
),
|
||||||
.configureBackgroundBackup(
|
dense: true,
|
||||||
triggerDelay: backupDelayToMilliseconds(v),
|
activeColor: activeColor,
|
||||||
onError: showErrorToUser,
|
value: isWifiRequired,
|
||||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
onChanged: hasExclusiveAccess
|
||||||
|
? (isChecked) => ref
|
||||||
|
.read(backupProvider.notifier)
|
||||||
|
.configureBackgroundBackup(
|
||||||
|
requireWifi: isChecked,
|
||||||
|
onError: showErrorToUser,
|
||||||
|
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (isBackgroundEnabled)
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text("backup_controller_page_background_charging")
|
||||||
|
.tr(),
|
||||||
|
secondary: Icon(
|
||||||
|
Icons.charging_station,
|
||||||
|
color: isChargingRequired ? activeColor : null,
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
activeColor: activeColor,
|
||||||
|
value: isChargingRequired,
|
||||||
|
onChanged: hasExclusiveAccess
|
||||||
|
? (isChecked) => ref
|
||||||
|
.read(backupProvider.notifier)
|
||||||
|
.configureBackgroundBackup(
|
||||||
|
requireCharging: isChecked,
|
||||||
|
onError: showErrorToUser,
|
||||||
|
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (isBackgroundEnabled && Platform.isAndroid)
|
||||||
|
ListTile(
|
||||||
|
isThreeLine: false,
|
||||||
|
dense: true,
|
||||||
|
enabled: hasExclusiveAccess,
|
||||||
|
title: const Text(
|
||||||
|
'backup_controller_page_background_delay',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
max: 3.0,
|
).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
|
||||||
divisions: 3,
|
subtitle: Slider(
|
||||||
label: formatBackupDelaySliderValue(triggerDelay.value),
|
value: triggerDelay.value,
|
||||||
activeColor: Theme.of(context).primaryColor,
|
onChanged: hasExclusiveAccess
|
||||||
|
? (double v) => triggerDelay.value = v
|
||||||
|
: null,
|
||||||
|
onChangeEnd: (double v) => ref
|
||||||
|
.read(backupProvider.notifier)
|
||||||
|
.configureBackgroundBackup(
|
||||||
|
triggerDelay: backupDelayToMilliseconds(v),
|
||||||
|
onError: showErrorToUser,
|
||||||
|
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||||
|
),
|
||||||
|
max: 3.0,
|
||||||
|
divisions: 3,
|
||||||
|
label: formatBackupDelaySliderValue(triggerDelay.value),
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () =>
|
||||||
|
ref.read(backupProvider.notifier).configureBackgroundBackup(
|
||||||
|
enabled: !isBackgroundEnabled,
|
||||||
|
onError: showErrorToUser,
|
||||||
|
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isBackgroundEnabled
|
||||||
|
? "backup_controller_page_background_turn_off"
|
||||||
|
: "backup_controller_page_background_turn_on",
|
||||||
|
style:
|
||||||
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||||
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () =>
|
|
||||||
ref.read(backupProvider.notifier).configureBackgroundBackup(
|
|
||||||
enabled: !isBackgroundEnabled,
|
|
||||||
onError: showErrorToUser,
|
|
||||||
onBatteryInfo: showBatteryOptimizationInfoToUser,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
isBackgroundEnabled
|
|
||||||
? "backup_controller_page_background_turn_off"
|
|
||||||
: "backup_controller_page_background_turn_on",
|
|
||||||
style:
|
|
||||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
|
||||||
).tr(),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
if (isBackgroundEnabled)
|
||||||
|
IosDebugInfoTile(
|
||||||
|
key: ValueKey(isChargingRequired),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,8 +601,8 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
buildAutoBackupController(),
|
buildAutoBackupController(),
|
||||||
if (Platform.isAndroid) const Divider(),
|
const Divider(),
|
||||||
if (Platform.isAndroid) buildBackgroundBackupController(),
|
buildBackgroundBackupController(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
buildStorageInformation(),
|
buildStorageInformation(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
@ -165,10 +165,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cancellation_token
|
name: cancellation_token
|
||||||
sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb"
|
sha256: e40ac742c7faac52e1719ce249934e20975e5772d40112e1e01cfc5abf24185a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.1"
|
version: "1.5.0"
|
||||||
cancellation_token_http:
|
cancellation_token_http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -333,10 +333,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fixnum
|
name: fixnum
|
||||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.0.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -428,10 +428,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
sha256: "0c997763ce06359ee4686553b74def84062e9d6929ac63f61fa02465c1f8e32c"
|
sha256: "46a27b7a11dc13738054093076f2dc65692ddcd463979b15092accf5681aea20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.2.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -483,10 +483,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: glob
|
name: glob
|
||||||
sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
|
sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.0"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -523,10 +523,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: hooks_riverpod
|
name: hooks_riverpod
|
||||||
sha256: "71695b2e1dfc22a39f1f9c67b798f8f8f1521f2d0349817d13ccdd5c4cd7acba"
|
sha256: a596bcb1eaf48eae6da1ce8b9e60ec9538ef7d15725e941c3626f29dfcc01d96
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.2.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -851,6 +851,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
path_provider_ios:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider_ios
|
||||||
|
sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.11"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -975,10 +983,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod
|
name: riverpod
|
||||||
sha256: "0f43c64f1f79c2112c843305a879a746587fb7c1e388f1d4717737796756e2c4"
|
sha256: "59a48de9c757aa61aa28e9fd625ffb360d43b6b54606f12536622c55be9e8c4b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.2.0"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1492,10 +1500,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
|
sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.2"
|
version: "6.1.0"
|
||||||
xxh3:
|
xxh3:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1513,5 +1521,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.0 <4.0.0"
|
dart: ">=2.18.0 <3.0.0"
|
||||||
flutter: ">=3.3.0"
|
flutter: ">=3.3.0"
|
||||||
|
@ -12,6 +12,7 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
path_provider_ios:
|
||||||
photo_manager: ^2.5.0
|
photo_manager: ^2.5.0
|
||||||
flutter_hooks: ^0.18.0
|
flutter_hooks: ^0.18.0
|
||||||
hooks_riverpod: ^2.0.0-dev.0
|
hooks_riverpod: ^2.0.0-dev.0
|
||||||
|
Loading…
Reference in New Issue
Block a user