diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore index f312f249a39ff..e32cadbf686e0 100644 --- a/mobile/ios/.gitignore +++ b/mobile/ios/.gitignore @@ -4,7 +4,6 @@ *.moved-aside *.pbxuser *.perspectivev3 -**/*sync/ .sconsign.dblite .tags* **/.vagrant/ diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 70bddbf10b997..472cf5d32d4eb 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 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 */; }; + 6FC4C0DB2CA3268700D44B0C /* BackgroundSyncShortcutIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC4C0DA2CA3268700D44B0C /* BackgroundSyncShortcutIntent.swift */; }; + 6FC4C0DD2CA32AF000D44B0C /* BackgroundSyncAppShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC4C0DC2CA32AF000D44B0C /* BackgroundSyncAppShortcut.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -19,6 +21,16 @@ /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ + 6FC4C0D82CA324C200D44B0C /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -38,6 +50,11 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = ""; }; 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = ""; }; + 6FC4C0AE2CA322AB00D44B0C /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 6FC4C0B62CA324C100D44B0C /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; + 6FC4C0C12CA324C200D44B0C /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; + 6FC4C0DA2CA3268700D44B0C /* BackgroundSyncShortcutIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSyncShortcutIntent.swift; sourceTree = ""; }; + 6FC4C0DC2CA32AF000D44B0C /* BackgroundSyncAppShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundSyncAppShortcut.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -80,6 +97,8 @@ isa = PBXGroup; children = ( 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */, + 6FC4C0B62CA324C100D44B0C /* Intents.framework */, + 6FC4C0C12CA324C200D44B0C /* IntentsUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -87,8 +106,10 @@ 65DD438629917FAD0047FFA8 /* BackgroundSync */ = { isa = PBXGroup; children = ( + 6FC4C0DA2CA3268700D44B0C /* BackgroundSyncShortcutIntent.swift */, 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */, 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */, + 6FC4C0DC2CA32AF000D44B0C /* BackgroundSyncAppShortcut.swift */, ); path = BackgroundSync; sourceTree = ""; @@ -126,6 +147,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 6FC4C0AE2CA322AB00D44B0C /* Runner.entitlements */, 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -156,6 +178,7 @@ 3B06AD1E1E4923F5004D2608 /* Thin Binary */, D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */, 6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */, + 6FC4C0D82CA324C200D44B0C /* Embed Foundation Extensions */, ); buildRules = ( ); @@ -173,6 +196,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1530; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -313,6 +337,8 @@ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, + 6FC4C0DB2CA3268700D44B0C /* BackgroundSyncShortcutIntent.swift in Sources */, + 6FC4C0DD2CA32AF000D44B0C /* BackgroundSyncAppShortcut.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -396,6 +422,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; @@ -539,8 +566,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 177; @@ -567,8 +596,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 177; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 05cb061ca58b2..42c5eaad20756 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -42,6 +42,10 @@ import permission_handler_apple } } + if #available(iOS 16.0, *) { + BackgroundSyncAppShortcut.updateAppShortcutParameters() + } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncAppShortcut.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncAppShortcut.swift new file mode 100644 index 0000000000000..71b9f2ea451c0 --- /dev/null +++ b/mobile/ios/Runner/BackgroundSync/BackgroundSyncAppShortcut.swift @@ -0,0 +1,21 @@ +// +// BackgroundSyncAppShortcut.swift +// Runner +// +// Created by Encotric on 24/09/2024. +// + +import AppIntents + + +@available(iOS 16.0, *) +struct BackgroundSyncAppShortcut: AppShortcutsProvider { + + @AppShortcutsBuilder static var appShortcuts: [AppShortcut] { + AppShortcut(intent: BackgroundSyncShortcutIntent(), phrases: [ + // TODO: localized title + "Upload gallery using \(.applicationName)"], systemImageName: "square.and.arrow.up.on.square") + } + +} + diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundSyncShortcutIntent.swift b/mobile/ios/Runner/BackgroundSync/BackgroundSyncShortcutIntent.swift new file mode 100644 index 0000000000000..97cf3d31fa32d --- /dev/null +++ b/mobile/ios/Runner/BackgroundSync/BackgroundSyncShortcutIntent.swift @@ -0,0 +1,29 @@ +// +// BackgroundSyncShortcutIntent.swift +// Runner +// +// Created by Encotric on 24/09/2024. +// + +import AppIntents +import SwiftUI + +@available(iOS 16.0, *) +struct BackgroundSyncShortcutIntent: AppIntent { + + // TODO: localized title and description + static var title: LocalizedStringResource = "Sync gallery" + + static var openAppWhenRun: Bool = true + + static var isDiscoverable: Bool = true + + func perform() async throws -> some IntentResult { + + let backgroundWorker = BackgroundSyncWorker { _ in () } + backgroundWorker.run(maxSeconds: nil) + + return .result() + } + +} diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index b684804037010..460dc9dddcbac 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -90,6 +90,10 @@ We need to manage backup your photos album NSPhotoLibraryUsageDescription We need to manage backup your photos album + NSUserActivityTypes + + IntentIntent + UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/mobile/ios/Runner/Runner.entitlements b/mobile/ios/Runner/Runner.entitlements index 0c67376ebacb4..21d95c45f32e1 100644 --- a/mobile/ios/Runner/Runner.entitlements +++ b/mobile/ios/Runner/Runner.entitlements @@ -1,5 +1,8 @@ - + + com.apple.developer.siri + + diff --git a/mobile/ios/Runner/RunnerProfile.entitlements b/mobile/ios/Runner/RunnerProfile.entitlements index 903def2af5306..434a016c89f92 100644 --- a/mobile/ios/Runner/RunnerProfile.entitlements +++ b/mobile/ios/Runner/RunnerProfile.entitlements @@ -4,5 +4,7 @@ aps-environment development + com.apple.developer.siri + diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index a0b6bf16c2304..47c5e8a44e736 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:cancellation_token_http/http.dart' as http; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -338,31 +339,29 @@ class BackupService { } } - final fileStream = file.openRead(); - final assetRawUploadData = http.MultipartFile( - "assetData", - fileStream, - file.lengthSync(), + final fileLength = file.lengthSync(); + + final (baseDir, dir, _) = await Task.split(file: file); + + final backgroundRequest = UploadTask( filename: originalFileName, - ); + baseDirectory: baseDir, + directory: dir, + url: '$savedEndpoint/assets', + httpRequestMethod: 'POST', + priority: 10, + ); // Priority 10 for testing purposes; maybe we could change that to a lower value later - final baseRequest = MultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/assets'), - onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), - ); - - baseRequest.headers.addAll(ApiService.getRequestHeaders()); - baseRequest.headers["Transfer-Encoding"] = "chunked"; - baseRequest.fields['deviceAssetId'] = asset.localId!; - baseRequest.fields['deviceId'] = deviceId; - baseRequest.fields['fileCreatedAt'] = + backgroundRequest.headers.addAll(ApiService.getRequestHeaders()); + backgroundRequest.headers["Transfer-Encoding"] = "chunked"; + backgroundRequest.fields['deviceAssetId'] = asset.localId!; + backgroundRequest.fields['deviceId'] = deviceId; + backgroundRequest.fields['fileCreatedAt'] = asset.fileCreatedAt.toUtc().toIso8601String(); - baseRequest.fields['fileModifiedAt'] = + backgroundRequest.fields['fileModifiedAt'] = asset.fileModifiedAt.toUtc().toIso8601String(); - baseRequest.fields['isFavorite'] = asset.isFavorite.toString(); - baseRequest.fields['duration'] = asset.duration.toString(); - baseRequest.files.add(assetRawUploadData); + backgroundRequest.fields['isFavorite'] = asset.isFavorite.toString(); + backgroundRequest.fields['duration'] = asset.duration.toString(); onCurrentAsset( CurrentUploadAsset( @@ -382,29 +381,37 @@ class BackupService { livePhotoVideoId = await uploadLivePhotoVideo( originalFileName, livePhotoFile, - baseRequest, + backgroundRequest, cancelToken, ); } if (livePhotoVideoId != null) { - baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; + backgroundRequest.fields['livePhotoVideoId'] = livePhotoVideoId; } - final response = await httpClient.send( - baseRequest, - cancellationToken: cancelToken, + final response = await FileDownloader().upload( + backgroundRequest, + onProgress: (percentage) => { + // onProgress returns a double in [0.0;1.0] for percentage + if (percentage > 0) + onProgress( + (percentage * fileLength).toInt(), + fileLength, + ), + }, ); - final responseBody = - jsonDecode(await response.stream.bytesToString()); + final responseBody = jsonDecode(response.responseBody ?? "{}"); - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - final errorMessage = error['message'] ?? error['error']; + if (response.status == TaskStatus.failed || + ![200, 201].contains(response.responseStatusCode)) { + final error = response.exception != null + ? response.exception!.description + : responseBody; debugPrint( - "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}", + "Error(${response.responseStatusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | $error", ); onError( @@ -414,11 +421,11 @@ class BackupService { fileCreatedAt: asset.fileCreatedAt, fileName: originalFileName, fileType: _getAssetType(candidate.asset.type), - errorMessage: errorMessage, + errorMessage: error, ), ); - if (errorMessage == "Quota has been exceeded!") { + if (error == "Quota has been exceeded!") { anyErrors = true; break; } @@ -427,7 +434,7 @@ class BackupService { } bool isDuplicate = false; - if (response.statusCode == 200) { + if (response.responseStatusCode == 200) { isDuplicate = true; duplicatedAssetIds.add(asset.localId!); } @@ -477,7 +484,7 @@ class BackupService { Future uploadLivePhotoVideo( String originalFileName, File? livePhotoVideoFile, - MultipartRequest baseRequest, + UploadTask baseRequest, http.CancellationToken cancelToken, ) async { if (livePhotoVideoFile == null) { @@ -487,35 +494,33 @@ class BackupService { originalFileName, p.extension(livePhotoVideoFile.path), ); - final fileStream = livePhotoVideoFile.openRead(); - final livePhotoRawUploadData = http.MultipartFile( - "assetData", - fileStream, - livePhotoVideoFile.lengthSync(), + + final (baseDir, dir, _) = await Task.split(file: livePhotoVideoFile); + + final backgroundRequest = UploadTask( filename: livePhotoTitle, - ); - final livePhotoReq = MultipartRequest( - baseRequest.method, - baseRequest.url, - onProgress: baseRequest.onProgress, + baseDirectory: baseDir, + directory: dir, + url: baseRequest.url, + httpRequestMethod: baseRequest.httpRequestMethod, + priority: baseRequest.priority, ) ..headers.addAll(baseRequest.headers) ..fields.addAll(baseRequest.fields); - livePhotoReq.files.add(livePhotoRawUploadData); + final response = await FileDownloader() + .upload(backgroundRequest); //TODO: onProgress callback? - var response = await httpClient.send( - livePhotoReq, - cancellationToken: cancelToken, - ); + var responseBody = jsonDecode(response.responseBody ?? "{}"); - var responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - var error = responseBody; + if (response.status == TaskStatus.failed || + ![200, 201].contains(response.responseStatusCode)) { + final error = response.exception != null + ? response.exception!.description + : responseBody; debugPrint( - "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}", + "Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | $error", ); }