diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index 63c21146f..000000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1 +0,0 @@
-* @Infomaniak/ios
diff --git a/.package.resolved b/.package.resolved
index bd560da22..36a0b84af 100644
--- a/.package.resolved
+++ b/.package.resolved
@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ProxymanApp/atlantis",
"state" : {
- "revision" : "131d757cf8e6e368ad338728379174f7cfff9326",
- "version" : "1.23.0"
+ "revision" : "5145a7041ec71421d09653db87dcc80c81792004",
+ "version" : "1.24.0"
}
},
{
@@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack",
"state" : {
- "revision" : "363ed23d19a931809ea834a7d722da830353806a",
- "version" : "3.8.2"
+ "revision" : "af4973721172dd7850a42a58a9ffcec0ec82e5a9",
+ "version" : "3.8.4"
}
},
{
@@ -86,8 +86,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Infomaniak/ios-core",
"state" : {
- "revision" : "504932a51c978b6aa0bbc086054875a36241674e",
- "version" : "5.0.1"
+ "revision" : "a1689a335a84f1396816d25955cfcd2069be62ae",
+ "version" : "6.2.0"
}
},
{
@@ -95,8 +95,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Infomaniak/ios-core-ui",
"state" : {
- "revision" : "2ade7b9cfc5942d1126570b40a6680e4ffcff105",
- "version" : "3.0.0"
+ "revision" : "d593bdc3d788cadc8856137ff6d2cffbfd707163",
+ "version" : "4.1.0"
}
},
{
@@ -113,8 +113,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Infomaniak/ios-login",
"state" : {
- "revision" : "ba2e72bc84a673d4e1626f898419da82b54baefb",
- "version" : "5.0.0"
+ "revision" : "94dee9d95d92c5fbe152476b78b04985fbddaa7c",
+ "version" : "6.0.1"
+ }
+ },
+ {
+ "identity" : "ios-version-checker",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Infomaniak/ios-version-checker",
+ "state" : {
+ "revision" : "867d65cd4fc23d2a58b35975742bb448fd686f2c",
+ "version" : "1.1.2"
}
},
{
@@ -122,8 +131,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
- "revision" : "277f1ab2c6664b19b4a412e32b094b201e2d5757",
- "version" : "7.10.0"
+ "revision" : "3ec0ab0bca4feb56e8b33e289c9496e89059dd08",
+ "version" : "7.10.2"
}
},
{
@@ -138,9 +147,9 @@
{
"identity" : "localizekit",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/PhilippeWeidmann/LocalizeKit",
+ "location" : "https://github.com/Infomaniak/LocalizeKit",
"state" : {
- "revision" : "1abb86ce614ae8d925bf82123c7360ab95827975",
+ "revision" : "a8f5b63a242a7cb5d8939db011fc9ee871a2a300",
"version" : "1.0.1"
}
},
@@ -194,8 +203,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-core.git",
"state" : {
- "revision" : "7227d6a447821c28895daa099b6c7cd4c99d461b",
- "version" : "13.25.1"
+ "revision" : "a5e87a39cffdcc591f3203c11cfca68100d0b9a6",
+ "version" : "13.26.0"
}
},
{
@@ -203,8 +212,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/realm/realm-swift",
"state" : {
- "revision" : "836cc4b8619886f979f8961c3f592a82b0741591",
- "version" : "10.45.3"
+ "revision" : "eafdd3720a8cc750bdd38bf776082d2c8cf743fc",
+ "version" : "10.46.0"
}
},
{
@@ -212,8 +221,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
- "revision" : "3b9a8e69ca296bd8cd0e317ad7a448e5daf4a342",
- "version" : "8.18.0"
+ "revision" : "b847a202a517a90763e8fd0656d8028aeee7b78d",
+ "version" : "8.20.0"
}
},
{
@@ -221,8 +230,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Infomaniak/SnackBar.swift",
"state" : {
- "revision" : "9a3c0c71796625ec3804df993483aa80266fa1ff",
- "version" : "1.1.0"
+ "revision" : "7d8d20af50c6b744aa9791b597f7efbd0a15add2",
+ "version" : "1.2.0"
}
},
{
@@ -230,8 +239,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SnapKit/SnapKit.git",
"state" : {
- "revision" : "f222cbdf325885926566172f6f5f06af95473158",
- "version" : "5.6.0"
+ "revision" : "e74fe2a978d1216c3602b129447c7301573cc2d8",
+ "version" : "5.7.0"
}
},
{
@@ -257,8 +266,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
- "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307",
- "version" : "1.0.5"
+ "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
+ "version" : "1.1.0"
}
},
{
@@ -273,10 +282,10 @@
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/apple/swift-log.git",
+ "location" : "https://github.com/apple/swift-log",
"state" : {
- "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed",
- "version" : "1.5.3"
+ "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5",
+ "version" : "1.5.4"
}
},
{
@@ -284,8 +293,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
- "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c",
- "version" : "2.62.0"
+ "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62",
+ "version" : "2.63.0"
}
},
{
@@ -293,8 +302,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
- "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9",
- "version" : "2.25.0"
+ "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5",
+ "version" : "2.26.0"
}
},
{
@@ -302,8 +311,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
- "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e",
- "version" : "1.20.0"
+ "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce",
+ "version" : "1.20.1"
}
},
{
@@ -315,6 +324,15 @@
"version" : "1.0.2"
}
},
+ {
+ "identity" : "swift-system",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-system.git",
+ "state" : {
+ "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496",
+ "version" : "1.2.1"
+ }
+ },
{
"identity" : "swiftregex",
"kind" : "remoteSourceControl",
diff --git a/Project.swift b/Project.swift
index 662cd093e..2dd4b6785 100644
--- a/Project.swift
+++ b/Project.swift
@@ -24,9 +24,9 @@ let project = Project(name: "kDrive",
packages: [
.package(url: "https://github.com/apple/swift-algorithms", .upToNextMajor(from: "1.2.0")),
.package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.2.2")),
- .package(url: "https://github.com/Infomaniak/ios-core", .upToNextMajor(from: "5.0.1")),
- .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "3.0.0")),
- .package(url: "https://github.com/Infomaniak/ios-login", .upToNextMajor(from: "5.0.0")),
+ .package(url: "https://github.com/Infomaniak/ios-core", .upToNextMajor(from: "6.2.0")),
+ .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "4.1.0")),
+ .package(url: "https://github.com/Infomaniak/ios-login", .upToNextMajor(from: "6.0.1")),
.package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/Infomaniak/swift-concurrency", .upToNextMajor(from: "0.0.4")),
.package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.43.0")),
@@ -35,7 +35,6 @@ let project = Project(name: "kDrive",
.package(url: "https://github.com/flowbe/MaterialOutlinedTextField", .upToNextMajor(from: "0.1.0")),
.package(url: "https://github.com/ProxymanApp/atlantis", .upToNextMajor(from: "1.3.0")),
.package(url: "https://github.com/ra1028/DifferenceKit", .upToNextMajor(from: "1.3.0")),
- .package(url: "https://github.com/PhilippeWeidmann/LocalizeKit", .upToNextMajor(from: "1.0.1")),
.package(url: "https://github.com/airbnb/lottie-ios", .upToNextMinor(from: "3.4.0")),
.package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", .upToNextMajor(from: "3.7.0")),
.package(url: "https://github.com/RomanTysiachnik/DropDown", .branch("master")),
@@ -46,7 +45,8 @@ let project = Project(name: "kDrive",
.package(url: "https://github.com/Cocoanetics/Kvitto", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/raspu/Highlightr", .upToNextMajor(from: "2.1.0")),
.package(url: "https://github.com/bmoliveira/MarkdownKit", .upToNextMajor(from: "1.7.0")),
- .package(url: "https://github.com/matomo-org/matomo-sdk-ios", .upToNextMajor(from: "7.5.1"))
+ .package(url: "https://github.com/matomo-org/matomo-sdk-ios", .upToNextMajor(from: "7.5.1")),
+ .package(url: "https://github.com/Infomaniak/ios-version-checker", .upToNextMajor(from: "1.1.2")),
],
targets: [
Target(name: "kDrive",
@@ -164,13 +164,13 @@ let project = Project(name: "kDrive",
.package(product: "InfomaniakDI"),
.package(product: "InfomaniakConcurrency"),
.package(product: "RealmSwift"),
- .package(product: "LocalizeKit"),
.package(product: "Kingfisher"),
.package(product: "DifferenceKit"),
.package(product: "CocoaLumberjackSwift"),
.package(product: "MaterialOutlinedTextField"),
.package(product: "SwiftRegex"),
- .package(product: "Sentry")
+ .package(product: "Sentry"),
+ .package(product: "VersionChecker")
]),
Target(name: "kDriveFileProvider",
platform: .iOS,
diff --git a/Tuist/ProjectDescriptionHelpers/Constants.swift b/Tuist/ProjectDescriptionHelpers/Constants.swift
index ce25a1306..0a9761d35 100644
--- a/Tuist/ProjectDescriptionHelpers/Constants.swift
+++ b/Tuist/ProjectDescriptionHelpers/Constants.swift
@@ -26,7 +26,7 @@ public enum Constants {
public static let baseSettings = SettingsDictionary()
.automaticCodeSigning(devTeam: "864VDCS2QY")
.currentProjectVersion("1")
- .marketingVersion("4.4.0")
+ .marketingVersion("4.4.2")
public static let deploymentTarget = DeploymentTarget.iOS(targetVersion: "13.4", devices: [.iphone, .ipad])
diff --git a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift
index 05f85e5c9..0cbd4d243 100644
--- a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift
+++ b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift
@@ -63,6 +63,7 @@ public extension Target {
infoPlist: .file(path: "\(name)/Info.plist"),
sources: [
"\(name)/**",
+ "kDrive/UI/Controller/DriveUpdateRequiredViewController.swift",
"kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift",
"kDrive/UI/Controller/Create File/FloatingPanelUtils.swift",
"kDrive/UI/Controller/Files/Categories/**",
diff --git a/kDrive/AppDelegate+BGAppRefresh.swift b/kDrive/AppDelegate+BGAppRefresh.swift
deleted file mode 100644
index d69094dd3..000000000
--- a/kDrive/AppDelegate+BGAppRefresh.swift
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- Infomaniak kDrive - iOS App
- Copyright (C) 2023 Infomaniak Network SA
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
- */
-
-import BackgroundTasks
-import CocoaLumberjackSwift
-import Foundation
-import InfomaniakDI
-import kDriveCore
-
-extension AppDelegate {
- /* To debug background tasks:
- Launch ->
- e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.infomaniak.background.refresh"]
- OR
- e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"]
-
- Force early termination ->
- e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.refresh"]
- OR
- e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"]
- */
-
- /// schedule background tasks
- func scheduleBackgroundRefresh() {
- Log.bgTaskScheduling("scheduleBackgroundRefresh")
- // List pictures + upload files (+pictures) / photoKit
- let backgroundRefreshRequest = BGAppRefreshTaskRequest(identifier: Constants.backgroundRefreshIdentifier)
- #if DEBUG
- // Required for debugging
- backgroundRefreshRequest.earliestBeginDate = Date()
- #else
- backgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60)
- #endif
-
- // Upload files (+pictures) / photokit
- let longBackgroundRefreshRequest = BGProcessingTaskRequest(identifier: Constants.longBackgroundRefreshIdentifier)
- #if DEBUG
- // Required for debugging
- longBackgroundRefreshRequest.earliestBeginDate = Date()
- #else
- longBackgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60)
- #endif
- longBackgroundRefreshRequest.requiresNetworkConnectivity = true
- longBackgroundRefreshRequest.requiresExternalPower = true
- do {
- try backgroundTaskScheduler.submit(backgroundRefreshRequest)
- Log.bgTaskScheduling("scheduled task: \(backgroundRefreshRequest)")
- try backgroundTaskScheduler.submit(longBackgroundRefreshRequest)
- Log.bgTaskScheduling("scheduled task: \(longBackgroundRefreshRequest)")
-
- } catch {
- Log.bgTaskScheduling("Error scheduling background task: \(error)", level: .error)
- }
- }
-
- /// Register BackgroundTasks in scheduler for later
- func registerBackgroundTasks() {
- Log.bgTaskScheduling("registerBackgroundTasks")
- var registered = backgroundTaskScheduler.register(
- forTaskWithIdentifier: Constants.backgroundRefreshIdentifier,
- using: nil
- ) { task in
- self.scheduleBackgroundRefresh()
- @InjectService var uploadQueue: UploadQueue
- task.expirationHandler = {
- Log.bgTaskScheduling("Task \(Constants.backgroundRefreshIdentifier) EXPIRED", level: .error)
- uploadQueue.suspendAllOperations()
- uploadQueue.rescheduleRunningOperations()
- task.setTaskCompleted(success: false)
- }
-
- self.handleBackgroundRefresh { _ in
- Log.bgTaskScheduling("Task \(Constants.backgroundRefreshIdentifier) completed with SUCCESS")
- task.setTaskCompleted(success: true)
- }
- }
- Log.bgTaskScheduling("Task \(Constants.backgroundRefreshIdentifier) registered ? \(registered)")
-
- registered = backgroundTaskScheduler.register(
- forTaskWithIdentifier: Constants.longBackgroundRefreshIdentifier,
- using: nil
- ) { task in
- self.scheduleBackgroundRefresh()
- @InjectService var uploadQueue: UploadQueue
- task.expirationHandler = {
- Log.bgTaskScheduling("Task \(Constants.longBackgroundRefreshIdentifier) EXPIRED", level: .error)
- uploadQueue.suspendAllOperations()
- uploadQueue.rescheduleRunningOperations()
- task.setTaskCompleted(success: false)
- }
-
- self.handleBackgroundRefresh { _ in
- Log.bgTaskScheduling("Task \(Constants.longBackgroundRefreshIdentifier) completed with SUCCESS")
- task.setTaskCompleted(success: true)
- }
- }
- Log.bgTaskScheduling("Task \(Constants.longBackgroundRefreshIdentifier) registered ? \(registered)")
- }
-
- func handleBackgroundRefresh(completion: @escaping (Bool) -> Void) {
- Log.bgTaskScheduling("handleBackgroundRefresh")
- // User installed the app but never logged in
- if accountManager.accounts.isEmpty {
- completion(false)
- return
- }
-
- Log.bgTaskScheduling("Enqueue new pictures")
- @InjectService var photoUploader: PhotoLibraryUploader
- photoUploader.scheduleNewPicturesForUpload()
-
- Log.bgTaskScheduling("Clean errors for all uploads")
- @InjectService var uploadQueue: UploadQueue
- uploadQueue.cleanNetworkAndLocalErrorsForAllOperations()
-
- Log.bgTaskScheduling("Reload operations in queue")
- uploadQueue.rebuildUploadQueueFromObjectsInRealm()
-
- Log.bgTaskScheduling("waitForCompletion")
- uploadQueue.waitForCompletion {
- completion(true)
- }
- }
-}
diff --git a/kDrive/AppDelegate+Launch.swift b/kDrive/AppDelegate+Launch.swift
index 0eb046353..405a54153 100644
--- a/kDrive/AppDelegate+Launch.swift
+++ b/kDrive/AppDelegate+Launch.swift
@@ -38,6 +38,8 @@ extension AppDelegate {
askUserToRemovePicturesIfNecessary()
case .onboarding:
showOnboarding()
+ case .updateRequired:
+ showUpdateRequired()
}
}
@@ -102,6 +104,16 @@ extension AppDelegate {
}
}
+ private func showUpdateRequired() {
+ guard let window else {
+ SentryDebug.captureNoWindow()
+ return
+ }
+
+ window.rootViewController = DriveUpdateRequiredViewController()
+ window.makeKeyAndVisible()
+ }
+
// MARK: Misc
private func requestAppStoreReview() {
@@ -193,7 +205,7 @@ extension AppDelegate {
}
group.leave()
}
- uploadQueue.saveToRealmAndAddToQueue(uploadFile: uploadFile, itemIdentifier: nil)
+ uploadQueue.saveToRealm(uploadFile, itemIdentifier: nil)
}
}
diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift
index e31bb955f..a34cd0b12 100644
--- a/kDrive/AppDelegate.swift
+++ b/kDrive/AppDelegate.swift
@@ -31,11 +31,12 @@ import os.log
import StoreKit
import UIKit
import UserNotifications
+import VersionChecker
@main
final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDelegate {
/// Making sure the DI is registered at a very early stage of the app launch.
- private let dependencyInjectionHook = EarlyDIHook()
+ private let dependencyInjectionHook = EarlyDIHook(context: .app)
private var reachabilityListener: ReachabilityListener!
private static let currentStateVersion = 4
@@ -49,9 +50,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
@LazyInjectService var backgroundUploadSessionManager: BackgroundUploadSessionManager
@LazyInjectService var backgroundDownloadSessionManager: BackgroundDownloadSessionManager
@LazyInjectService var photoLibraryUploader: PhotoLibraryUploader
- @LazyInjectService var backgroundTaskScheduler: BGTaskScheduler
@LazyInjectService var notificationHelper: NotificationsHelpable
@LazyInjectService var accountManager: AccountManageable
+ @LazyInjectService var backgroundTasksService: BackgroundTasksServiceable
// MARK: - UIApplicationDelegate
@@ -71,7 +72,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
SentryDebug.capture(error: error)
}
- registerBackgroundTasks()
+ backgroundTasksService.registerBackgroundTasks()
// In some cases the application can show the old Nextcloud import notification badge
UIApplication.shared.applicationIconBadgeNumber = 0
@@ -167,8 +168,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
func applicationDidEnterBackground(_ application: UIApplication) {
Log.appDelegate("applicationDidEnterBackground")
+ backgroundTasksService.scheduleBackgroundRefresh()
- scheduleBackgroundRefresh()
if UserDefaults.shared.isAppLockEnabled,
!(window?.rootViewController?.isKind(of: LockedAppViewController.self) ?? false) {
lockHelper.setTime()
@@ -183,19 +184,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
shortcutItemToProcess = shortcutItem
}
- func application(_ application: UIApplication,
- performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
- Log.appDelegate("application performFetchWithCompletionHandler")
-
- handleBackgroundRefresh { newData in
- if newData {
- completionHandler(.newData)
- } else {
- completionHandler(.noData)
- }
- }
- }
-
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
Log.appDelegate("application app open url\(url)")
@@ -218,12 +206,18 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
UserDefaults.shared.numberOfConnections += 1
refreshCacheScanLibraryAndUpload(preload: false, isSwitching: false)
uploadEditedFiles()
- case .onboarding: break
+ case .onboarding, .updateRequired: break
// NOOP
}
// Remove all notifications on App Opening
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
+
+ Task {
+ if try await VersionChecker.standard.checkAppVersionStatus() == .updateIsRequired {
+ prepareRootViewController(currentState: .updateRequired)
+ }
+ }
}
/// Set global tint color
@@ -390,14 +384,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
func setRootViewController(_ vc: UIViewController,
animated: Bool = true) {
- guard animated, let window else {
- self.window?.rootViewController = vc
- self.window?.makeKeyAndVisible()
+ guard let window else {
return
}
window.rootViewController = vc
window.makeKeyAndVisible()
+
+ guard animated else {
+ return
+ }
+
UIView.transition(with: window, duration: 0.3,
options: .transitionCrossDissolve,
animations: nil,
@@ -430,9 +427,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
viewController: fileListViewController)
} else {
let filePresenter = FilePresenter(viewController: fileListViewController)
- filePresenter.present(driveFileManager: driveFileManager,
- file: file,
+ filePresenter.present(for: file,
files: [file],
+ driveFileManager: driveFileManager,
normalFolderHierarchy: false)
}
}
@@ -471,11 +468,15 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
- let encodedVersion = coder.decodeInteger(forKey: AppDelegate.appStateVersionKey)
+ // App Restore disabled until we rework it
+ return false
+
+ /*
+ let encodedVersion = coder.decodeInteger(forKey: AppDelegate.appStateVersionKey)
- return AppDelegate
- .currentStateVersion == encodedVersion &&
- !(UserDefaults.shared.legacyIsFirstLaunch || accountManager.accounts.isEmpty)
+ return AppDelegate
+ .currentStateVersion == encodedVersion &&
+ !(UserDefaults.shared.legacyIsFirstLaunch || accountManager.accounts.isEmpty)*/
}
// MARK: - User activity
diff --git a/kDrive/Resources/Assets.xcassets/update-required.imageset/Contents.json b/kDrive/Resources/Assets.xcassets/update-required.imageset/Contents.json
new file mode 100644
index 000000000..d51d2bd80
--- /dev/null
+++ b/kDrive/Resources/Assets.xcassets/update-required.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "filename" : "update-required-light.svg",
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "filename" : "update-required-dark.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/kDrive/Resources/Assets.xcassets/update-required.imageset/update-required-dark.svg b/kDrive/Resources/Assets.xcassets/update-required.imageset/update-required-dark.svg
new file mode 100644
index 000000000..df8283ef2
--- /dev/null
+++ b/kDrive/Resources/Assets.xcassets/update-required.imageset/update-required-dark.svg
@@ -0,0 +1,28 @@
+
diff --git a/kDrive/Resources/Assets.xcassets/update-required.imageset/update-required-light.svg b/kDrive/Resources/Assets.xcassets/update-required.imageset/update-required-light.svg
new file mode 100644
index 000000000..129bf2360
--- /dev/null
+++ b/kDrive/Resources/Assets.xcassets/update-required.imageset/update-required-light.svg
@@ -0,0 +1,28 @@
+
diff --git a/kDrive/UI/Controller/DriveUpdateRequiredViewController.swift b/kDrive/UI/Controller/DriveUpdateRequiredViewController.swift
new file mode 100644
index 000000000..08d942a28
--- /dev/null
+++ b/kDrive/UI/Controller/DriveUpdateRequiredViewController.swift
@@ -0,0 +1,82 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2023 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import InfomaniakCoreUI
+import kDriveCore
+import kDriveResources
+import SwiftUI
+import UIKit
+import VersionChecker
+
+class DriveUpdateRequiredViewController: UIViewController {
+ var dismissHandler: (() -> Void)?
+
+ private let sharedStyle: TemplateSharedStyle = {
+ let largeButtonStyle = IKLargeButton.Style.primaryButton
+ return TemplateSharedStyle(
+ background: KDriveResourcesAsset.backgroundColor.swiftUIColor,
+ titleTextStyle: .init(font: Font(TextStyle.header2.font), color: Color(TextStyle.header2.color)),
+ descriptionTextStyle: .init(font: Font(TextStyle.body1.font), color: Color(TextStyle.body1.color)),
+ buttonStyle: .init(
+ background: Color(largeButtonStyle.backgroundColor),
+ textStyle: .init(font: Font(largeButtonStyle.titleFont), color: Color(largeButtonStyle.titleColor)),
+ height: 60,
+ radius: UIConstants.buttonCornerRadius
+ )
+ )
+ }()
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let hostingViewController = UIHostingController(rootView: UpdateRequiredView(
+ image: KDriveResourcesAsset.updateRequired.swiftUIImage,
+ sharedStyle: sharedStyle,
+ updateHandler: updateApp,
+ dismissHandler: dismissHandler
+ ))
+ guard let hostingView = hostingViewController.view else { return }
+
+ hostingView.translatesAutoresizingMaskIntoConstraints = false
+ addChild(hostingViewController)
+ view.addSubview(hostingView)
+
+ NSLayoutConstraint.activate([
+ hostingView.topAnchor.constraint(equalTo: view.topAnchor),
+ hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ hostingView.leftAnchor.constraint(equalTo: view.leftAnchor),
+ hostingView.rightAnchor.constraint(equalTo: view.rightAnchor)
+ ])
+
+ hostingViewController.didMove(toParent: self)
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ navigationController?.setNavigationBarHidden(true, animated: animated)
+ }
+
+ private func updateApp() {
+ let storeURL: URLConstants = Bundle.main.isRunningInTestFlight ? .testFlight : .appStore
+ UIConstants.openUrl(storeURL.url, from: self)
+ }
+}
+
+@available(iOS 17.0, *)
+#Preview {
+ return DriveUpdateRequiredViewController()
+}
diff --git a/kDrive/UI/Controller/Files/File List/DragAndDropFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/DragAndDropFileListViewModel.swift
index 785b56102..57af7776a 100644
--- a/kDrive/UI/Controller/Files/File List/DragAndDropFileListViewModel.swift
+++ b/kDrive/UI/Controller/Files/File List/DragAndDropFileListViewModel.swift
@@ -139,10 +139,11 @@ final class DroppableFileListViewModel {
let drive = driveFileManager.drive
Task {
do {
- try await self.fileImportHelper.upload(
- files: importedFiles,
+ try await self.fileImportHelper.saveForUpload(
+ importedFiles,
in: frozenDestination,
- drive: drive
+ drive: drive,
+ addToQueue: true
)
} catch {
UIConstants.showSnackBarIfNeeded(error: error)
diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift
index aeaf3bceb..d2c7679fb 100644
--- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift
+++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift
@@ -582,9 +582,9 @@ class FileListViewController: UIViewController, UICollectionViewDataSource, Swip
func onFilePresented(_ file: File) {
#if !ISEXTENSION
- filePresenter.present(driveFileManager: viewModel.driveFileManager,
- file: file,
+ filePresenter.present(for: file,
files: viewModel.getAllFiles(),
+ driveFileManager: viewModel.driveFileManager,
normalFolderHierarchy: viewModel.configuration.normalFolderHierarchy,
fromActivities: viewModel.configuration.fromActivities)
#endif
diff --git a/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift
index 27877bdc2..e82163d25 100644
--- a/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift
+++ b/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift
@@ -25,7 +25,14 @@ class InMemoryFileListViewModel: FileListViewModel {
private let realm: Realm
override init(configuration: Configuration, driveFileManager: DriveFileManager, currentDirectory: File) {
- if let realm = currentDirectory.realm {
+ // TODO: Refactor to explicit realm state
+ /// We expect the object to be live in this view controller, if not detached.
+ var currentDirectory = currentDirectory
+ if currentDirectory.isFrozen, let liveDirectory = currentDirectory.thaw() {
+ currentDirectory = liveDirectory
+ }
+
+ if let realm = currentDirectory.realm, !currentDirectory.isFrozen {
self.realm = realm
} else {
let unCachedRealmConfiguration = Realm.Configuration(
@@ -40,6 +47,7 @@ class InMemoryFileListViewModel: FileListViewModel {
}
super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory)
+
try? realm.write {
realm.add(currentDirectory)
}
diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift
index 5b647da48..bd7a46aa1 100644
--- a/kDrive/UI/Controller/Files/FilePresenter.swift
+++ b/kDrive/UI/Controller/Files/FilePresenter.swift
@@ -63,18 +63,16 @@ final class FilePresenter {
func presentParent(of file: File, driveFileManager: DriveFileManager, animated: Bool = true) {
if let parent = file.parent {
- present(driveFileManager: driveFileManager, file: parent, files: [], normalFolderHierarchy: true, animated: animated)
+ present(for: parent, files: [], driveFileManager: driveFileManager, normalFolderHierarchy: true, animated: animated)
} else if file.parentId != 0 {
Task {
do {
let parent = try await driveFileManager.file(id: file.parentId)
- present(
- driveFileManager: driveFileManager,
- file: parent,
- files: [],
- normalFolderHierarchy: true,
- animated: animated
- )
+ present(for: parent,
+ files: [],
+ driveFileManager: driveFileManager,
+ normalFolderHierarchy: true,
+ animated: animated)
} catch {
UIConstants.showSnackBarIfNeeded(error: error)
}
@@ -84,93 +82,124 @@ final class FilePresenter {
}
}
- func present(driveFileManager: DriveFileManager,
- file: File,
+ func present(for file: File,
files: [File],
+ driveFileManager: DriveFileManager,
normalFolderHierarchy: Bool,
fromActivities: Bool = false,
animated: Bool = true,
completion: ((Bool) -> Void)? = nil) {
if file.isDirectory {
- // Show files list
- let viewModel: FileListViewModel
- if driveFileManager.drive.sharedWithMe {
- viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file)
- } else if file.isTrashed || file.deletedAt != nil {
- viewModel = TrashListViewModel(driveFileManager: driveFileManager, currentDirectory: file)
- } else {
- viewModel = ConcreteFileListViewModel(driveFileManager: driveFileManager, currentDirectory: file)
- }
- let nextVC = FileListViewController.instantiate(viewModel: viewModel)
- if file.isDisabled {
- if driveFileManager.drive.isUserAdmin {
- let accessFileDriveFloatingPanelController = AccessFileFloatingPanelViewController.instantiatePanel()
- let floatingPanelViewController = accessFileDriveFloatingPanelController
- .contentViewController as? AccessFileFloatingPanelViewController
- floatingPanelViewController?.actionHandler = { [weak self] _ in
- guard let self else { return }
- floatingPanelViewController?.rightButton.setLoading(true)
- Task { [proxyFile = file.proxify()] in
- do {
- let response = try await driveFileManager.apiFetcher.forceAccess(to: proxyFile)
- if response {
- accessFileDriveFloatingPanelController.dismiss(animated: true)
- self.navigationController?.pushViewController(nextVC, animated: true)
- } else {
- UIConstants.showSnackBar(message: KDriveResourcesStrings.Localizable.errorRightModification)
- }
- } catch {
- UIConstants.showSnackBarIfNeeded(error: error)
+ presentDirectory(for: file, driveFileManager: driveFileManager, animated: animated, completion: completion)
+ } else if file.isBookmark {
+ downloadAndPresentBookmark(for: file, completion: completion)
+ } else {
+ presentFile(
+ for: file,
+ files: files,
+ driveFileManager: driveFileManager,
+ normalFolderHierarchy: normalFolderHierarchy,
+ fromActivities: fromActivities,
+ animated: animated,
+ completion: completion
+ )
+ }
+ }
+
+ private func presentFile(for file: File,
+ files: [File],
+ driveFileManager: DriveFileManager,
+ normalFolderHierarchy: Bool,
+ fromActivities: Bool,
+ animated: Bool,
+ completion: ((Bool) -> Void)?) {
+ // Show file preview
+ let files = files.filter { !$0.isDirectory && !$0.isTrashed }
+ if let index = files.firstIndex(where: { $0.id == file.id }) {
+ let previewViewController = PreviewViewController.instantiate(
+ files: files,
+ index: Int(index),
+ driveFileManager: driveFileManager,
+ normalFolderHierarchy: normalFolderHierarchy,
+ fromActivities: fromActivities
+ )
+ navigationController?.pushViewController(previewViewController, animated: animated)
+ completion?(true)
+ }
+ if file.isTrashed {
+ UIConstants.showSnackBar(message: KDriveResourcesStrings.Localizable.errorPreviewTrash)
+ completion?(false)
+ }
+ }
+
+ private func presentDirectory(
+ for file: File,
+ driveFileManager: DriveFileManager,
+ animated: Bool,
+ completion: ((Bool) -> Void)?
+ ) {
+ // Show files list
+ let viewModel: FileListViewModel
+ if driveFileManager.drive.sharedWithMe {
+ viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file)
+ } else if file.isTrashed || file.deletedAt != nil {
+ viewModel = TrashListViewModel(driveFileManager: driveFileManager, currentDirectory: file)
+ } else {
+ viewModel = ConcreteFileListViewModel(driveFileManager: driveFileManager, currentDirectory: file)
+ }
+ let nextVC = FileListViewController.instantiate(viewModel: viewModel)
+ if file.isDisabled {
+ if driveFileManager.drive.isUserAdmin {
+ let accessFileDriveFloatingPanelController = AccessFileFloatingPanelViewController.instantiatePanel()
+ let floatingPanelViewController = accessFileDriveFloatingPanelController
+ .contentViewController as? AccessFileFloatingPanelViewController
+ floatingPanelViewController?.actionHandler = { [weak self] _ in
+ guard let self else { return }
+ floatingPanelViewController?.rightButton.setLoading(true)
+ Task { [proxyFile = file.proxify()] in
+ do {
+ let response = try await driveFileManager.apiFetcher.forceAccess(to: proxyFile)
+ if response {
+ accessFileDriveFloatingPanelController.dismiss(animated: true)
+ self.navigationController?.pushViewController(nextVC, animated: true)
+ } else {
+ UIConstants.showSnackBar(message: KDriveResourcesStrings.Localizable.errorRightModification)
}
+ } catch {
+ UIConstants.showSnackBarIfNeeded(error: error)
}
}
- viewController?.present(accessFileDriveFloatingPanelController, animated: true)
- } else {
- viewController?.present(NoAccessFloatingPanelViewController.instantiatePanel(), animated: true)
}
+ viewController?.present(accessFileDriveFloatingPanelController, animated: true)
} else {
- navigationController?.pushViewController(nextVC, animated: animated)
+ viewController?.present(NoAccessFloatingPanelViewController.instantiatePanel(), animated: true)
}
- completion?(true)
- } else if file.isBookmark {
- // Open bookmark URL
- if file.isMostRecentDownloaded {
- presentBookmark(for: file, completion: completion)
- } else {
- // Download file
- DownloadQueue.instance.temporaryDownload(file: file, userId: accountManager.currentUserId) { error in
- Task {
- if let error {
- UIConstants.showSnackBarIfNeeded(error: error)
- completion?(false)
- } else {
- self.presentBookmark(for: file, completion: completion)
- }
+ } else {
+ navigationController?.pushViewController(nextVC, animated: animated)
+ }
+ completion?(true)
+ }
+
+ private func downloadAndPresentBookmark(for file: File, completion: ((Bool) -> Void)?) {
+ // Open bookmark URL
+ if file.isMostRecentDownloaded {
+ presentBookmark(for: file, completion: completion)
+ } else {
+ // Download file
+ DownloadQueue.instance.temporaryDownload(file: file, userId: accountManager.currentUserId) { error in
+ Task {
+ if let error {
+ UIConstants.showSnackBarIfNeeded(error: error)
+ completion?(false)
+ } else {
+ self.presentBookmark(for: file, completion: completion)
}
}
}
- } else {
- // Show file preview
- let files = files.filter { !$0.isDirectory && !$0.isTrashed }
- if let index = files.firstIndex(where: { $0.id == file.id }) {
- let previewViewController = PreviewViewController.instantiate(
- files: files,
- index: Int(index),
- driveFileManager: driveFileManager,
- normalFolderHierarchy: normalFolderHierarchy,
- fromActivities: fromActivities
- )
- navigationController?.pushViewController(previewViewController, animated: animated)
- completion?(true)
- }
- if file.isTrashed {
- UIConstants.showSnackBar(message: KDriveResourcesStrings.Localizable.errorPreviewTrash)
- completion?(false)
- }
}
}
- private func presentBookmark(for file: File, animated: Bool = true, completion: ((Bool) -> Void)? = nil) {
+ private func presentBookmark(for file: File, animated: Bool = true, completion: ((Bool) -> Void)?) {
if let url = file.getBookmarkURL() {
if url.scheme == "http" || url.scheme == "https" {
let safariViewController = SFSafariViewController(url: url)
diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift
index 90e52243d..500b0c28b 100644
--- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift
+++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift
@@ -402,9 +402,9 @@ class PreviewViewController: UIViewController, PreviewContentCellDelegate {
if currentFile.isBookmark {
floatingPanelViewController.dismiss(animated: false)
FilePresenter(viewController: self).present(
- driveFileManager: driveFileManager,
- file: currentFile,
+ for: currentFile,
files: [],
+ driveFileManager: driveFileManager,
normalFolderHierarchy: true
) { success in
if !success {
diff --git a/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift b/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift
index ec7ad9407..f0b7c648e 100644
--- a/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift
+++ b/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift
@@ -113,9 +113,9 @@ class RecentActivityFilesViewController: FileListViewController {
#if !ISEXTENSION
if file.isDirectory {
let managedFile = driveFileManager.getManagedFile(from: file.detached())
- filePresenter.present(driveFileManager: viewModel.driveFileManager,
- file: managedFile,
+ filePresenter.present(for: managedFile,
files: viewModel.getAllFiles(),
+ driveFileManager: viewModel.driveFileManager,
normalFolderHierarchy: viewModel.configuration.normalFolderHierarchy,
fromActivities: viewModel.configuration.fromActivities)
} else {
diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift
new file mode 100644
index 000000000..b0992a25c
--- /dev/null
+++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift
@@ -0,0 +1,100 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2024 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakCoreUI
+import InfomaniakDI
+import kDriveCore
+import kDriveResources
+import UIKit
+
+// MARK: - FooterButtonDelegate
+
+extension SaveFileViewController: FooterButtonDelegate {
+ @objc func didClickOnButton(_ sender: AnyObject) {
+ guard let drive = selectedDriveFileManager?.drive,
+ let directory = selectedDirectory else {
+ return
+ }
+
+ // Making sure the user cannot spam the button on tasks that may take a while
+ let button = sender as? IKLargeButton
+ button?.setLoading(true)
+
+ let items = items
+ guard !items.isEmpty else {
+ dismiss(animated: true)
+ return
+ }
+
+ Task {
+ await presentSnackBarSaveAndDismiss(files: items, directory: directory, drive: drive)
+ }
+ }
+
+ private func presentSnackBarSaveAndDismiss(files: [ImportedFile], directory: File, drive: Drive) async {
+ let message: String
+ do {
+ try await processForUpload(files: files, directory: directory, drive: drive)
+
+ message = files.count > 1 ? KDriveResourcesStrings.Localizable
+ .allUploadInProgressPlural(files.count) : KDriveResourcesStrings.Localizable
+ .allUploadInProgress(files[0].name)
+ } catch {
+ message = error.localizedDescription
+ }
+
+ Task { @MainActor in
+ self.dismiss(animated: true, clean: false) {
+ UIConstants.showSnackBar(message: message)
+ }
+ }
+ }
+
+ private func processForUpload(files: [ImportedFile], directory: File, drive: Drive) async throws {
+ // We only schedule for uploading in main app target
+ let addToQueue = !appContextService.isExtension
+ try await fileImportHelper.saveForUpload(files, in: directory, drive: drive, addToQueue: addToQueue)
+ #if ISEXTENSION
+ showOpenAppToContinueNotification()
+ #endif
+ }
+
+ #if ISEXTENSION
+ // Dynamic hook to open an URL within an extension
+ @objc func openURL(_ url: URL) -> Bool {
+ var responder: UIResponder? = self
+ while responder != nil {
+ if let application = responder as? UIApplication {
+ return application.perform(#selector(openURL(_:)), with: url) != nil
+ }
+ responder = responder?.next
+ }
+ return false
+ }
+
+ func showOpenAppToContinueNotification() {
+ guard openURL(URLConstants.kDriveRedirection.url) else {
+ // Fallback on a local notification if failure to open URL
+ @InjectService var notificationHelper: NotificationsHelpable
+ notificationHelper.sendPausedUploadQueueNotification()
+ return
+ }
+ }
+ #endif
+}
diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift
index fe0d1116a..8384f9293 100644
--- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift
+++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift
@@ -28,6 +28,7 @@ import UIKit
class SaveFileViewController: UIViewController {
@LazyInjectService var accountManager: AccountManageable
@LazyInjectService var fileImportHelper: FileImportHelper
+ @LazyInjectService var appContextService: AppContextServiceable
enum SaveFileSection {
case alert
@@ -181,6 +182,19 @@ class SaveFileViewController: UIViewController {
}
}
+ func dismiss(animated: Bool, clean: Bool = true, completion: (() -> Void)? = nil) {
+ Task {
+ // Cleanup file that were duplicated to appGroup on extension mode
+ if appContextService.isExtension && clean {
+ await items.concurrentForEach { item in
+ try? FileManager.default.removeItem(at: item.path)
+ }
+ }
+
+ navigationController?.dismiss(animated: animated, completion: completion)
+ }
+ }
+
deinit {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
@@ -275,7 +289,7 @@ class SaveFileViewController: UIViewController {
@IBAction func close(_ sender: Any) {
importProgress?.cancel()
- navigationController?.dismiss(animated: true)
+ dismiss(animated: true)
}
}
@@ -474,43 +488,3 @@ extension SaveFileViewController: SelectPhotoFormatDelegate {
}
}
}
-
-// MARK: - FooterButtonDelegate
-
-extension SaveFileViewController: FooterButtonDelegate {
- @objc func didClickOnButton(_ sender: AnyObject) {
- guard let selectedDriveFileManager,
- let selectedDirectory else {
- return
- }
-
- // Making sure the user cannot spam the button on tasks that may take a while
- let button = sender as? IKLargeButton
- button?.setLoading(true)
-
- let items = items
- guard !items.isEmpty else {
- navigationController?.dismiss(animated: true)
- return
- }
-
- Task {
- let message: String
- do {
- try await fileImportHelper.upload(files: items, in: selectedDirectory, drive: selectedDriveFileManager.drive)
-
- message = items.count > 1 ? KDriveResourcesStrings.Localizable
- .allUploadInProgressPlural(items.count) : KDriveResourcesStrings.Localizable
- .allUploadInProgress(items[0].name)
- } catch {
- message = error.localizedDescription
- }
-
- Task { @MainActor in
- self.navigationController?.dismiss(animated: true) {
- UIConstants.showSnackBar(message: message)
- }
- }
- }
- }
-}
diff --git a/kDrive/UI/Controller/Home/HomeViewController.swift b/kDrive/UI/Controller/Home/HomeViewController.swift
index eec71e634..edab7ecef 100644
--- a/kDrive/UI/Controller/Home/HomeViewController.swift
+++ b/kDrive/UI/Controller/Home/HomeViewController.swift
@@ -712,9 +712,9 @@ extension HomeViewController {
switch viewModel.recentFiles {
case .file(let files):
filePresenter.present(
- driveFileManager: driveFileManager,
- file: files[indexPath.row],
+ for: files[indexPath.row],
files: files,
+ driveFileManager: driveFileManager,
normalFolderHierarchy: false
)
case .fileActivity:
@@ -788,9 +788,9 @@ extension HomeViewController: RecentActivityDelegate {
filePresenter.navigationController?.pushViewController(nextVC, animated: true)
} else {
filePresenter.present(
- driveFileManager: driveFileManager,
- file: driveFileManager.getManagedFile(from: file),
+ for: driveFileManager.getManagedFile(from: file),
files: activities.compactMap(\.file),
+ driveFileManager: driveFileManager,
normalFolderHierarchy: false
)
}
diff --git a/kDrive/UI/Controller/MainTabViewController.swift b/kDrive/UI/Controller/MainTabViewController.swift
index e74857a7b..7e98e006c 100644
--- a/kDrive/UI/Controller/MainTabViewController.swift
+++ b/kDrive/UI/Controller/MainTabViewController.swift
@@ -277,14 +277,15 @@ extension MainTabViewController: UIDocumentPickerDelegate {
}
try FileManager.default.moveItem(at: url, to: targetURL)
- uploadQueue.saveToRealmAndAddToQueue(uploadFile:
+ uploadQueue.saveToRealm(
UploadFile(
parentDirectoryId: documentPicker.importDriveDirectory.id,
userId: accountManager.currentUserId,
driveId: documentPicker.importDrive.id,
url: targetURL,
name: url.lastPathComponent
- ))
+ )
+ )
} catch {
UIConstants.showSnackBarIfNeeded(error: DriveError.unknownError)
}
diff --git a/kDrive/UI/Controller/RegisterViewController.swift b/kDrive/UI/Controller/RegisterViewController.swift
index ee948fcbc..3abaa7b2e 100644
--- a/kDrive/UI/Controller/RegisterViewController.swift
+++ b/kDrive/UI/Controller/RegisterViewController.swift
@@ -123,7 +123,7 @@ extension RegisterViewController: WKNavigationDelegate {
) {
guard let host = navigationAction.request.url?.host,
let kDriveHost = URLConstants.kDriveWeb.url.host,
- let loginHost = URL(string: InfomaniakCore.Constants.LOGIN_URL)?.host else {
+ let loginHost = infomaniakLogin.config.loginURL.host else {
decisionHandler(.allow)
return
}
diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift
index 548cb3c02..479795f89 100644
--- a/kDrive/UI/View/Files/FileCollectionViewCell.swift
+++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift
@@ -47,6 +47,7 @@ protocol FileCellDelegate: AnyObject {
]
var file: File
var selectionMode: Bool
+ var isSelected = false
private var downloadProgressObserver: ObservationToken?
private var downloadObserver: ObservationToken?
var thumbnailDownloadTask: Kingfisher.DownloadTask?
@@ -148,7 +149,8 @@ protocol FileCellDelegate: AnyObject {
// Fetch thumbnail
thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in
guard let self,
- !self.file.isInvalidated else {
+ !self.file.isInvalidated,
+ !self.isSelected else {
return
}
@@ -190,12 +192,13 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
@IBOutlet weak var downloadProgressView: RPCircularProgress?
@IBOutlet weak var highlightedView: UIView!
- var viewModel: FileViewModel!
+ var viewModel: FileViewModel?
weak var delegate: FileCellDelegate?
override var isSelected: Bool {
didSet {
+ viewModel?.isSelected = isSelected
configureForSelection()
}
}
@@ -323,13 +326,14 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
/// Update the cell selection mode.
/// - Parameter selectionMode: The new selection mode (enabled/disabled).
func setSelectionMode(_ selectionMode: Bool) {
- guard viewModel != nil else { return }
+ guard let viewModel else { return }
viewModel.selectionMode = selectionMode
configure(with: viewModel)
}
func configureForSelection() {
guard viewModel?.selectionMode == true else { return }
+
if isSelected {
configureCheckmarkImage()
configureImport(shouldDisplay: false)
@@ -339,6 +343,7 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
}
private func configureLogoImage() {
+ guard let viewModel else { return }
logoImage.isAccessibilityElement = true
logoImage.accessibilityLabel = viewModel.iconAccessibilityLabel
logoImage.image = viewModel.icon
@@ -360,6 +365,8 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
}
func configureImport(shouldDisplay: Bool) {
+ guard let viewModel else { return }
+
if shouldDisplay && viewModel.isImporting {
logoImage.isHidden = true
importProgressView?.isHidden = false
@@ -391,10 +398,13 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
extension FileCollectionViewCell: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
- return min(viewModel.categories.count, 3)
+ return min(viewModel?.categories.count ?? 0, 3)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ guard let viewModel else {
+ return UICollectionViewCell()
+ }
let cell = collectionView.dequeueReusableCell(type: CategoryBadgeCollectionViewCell.self, for: indexPath)
let category = viewModel.categories[indexPath.row]
let more = indexPath.item == 2 && viewModel.categories.count > 3 ? viewModel.categories.count - 3 : nil
diff --git a/kDrive/Utils/AppFactoryService.swift b/kDrive/Utils/AppFactoryService.swift
index 5b63f7ac8..155c601c1 100644
--- a/kDrive/Utils/AppFactoryService.swift
+++ b/kDrive/Utils/AppFactoryService.swift
@@ -23,14 +23,16 @@ import os.log
/// Something that loads the DI on init
public struct EarlyDIHook {
- public init() {
- // setup DI ASAP
+ public init(context: DriveAppContext) {
+ os_log("EarlyDIHook")
- let navigationManagerFactory = Factory(type: NavigationManageable.self) { _, _ in
+ let extraDependencies = [Factory(type: NavigationManageable.self) { _, _ in
NavigationManager()
- }
+ }, Factory(type: AppContextServiceable.self) { _, _ in
+ AppContextService(context: context)
+ }]
- os_log("EarlyDIHook")
- FactoryService.setupDependencyInjection(other: [navigationManagerFactory])
+ // setup DI ASAP
+ FactoryService.setupDependencyInjection(other: extraDependencies)
}
}
diff --git a/kDrive/Utils/RootViewControllerState.swift b/kDrive/Utils/RootViewControllerState.swift
index 03722e027..ca43a476e 100644
--- a/kDrive/Utils/RootViewControllerState.swift
+++ b/kDrive/Utils/RootViewControllerState.swift
@@ -26,6 +26,7 @@ enum RootViewControllerState {
case onboarding
case appLock
case mainViewController(DriveFileManager)
+ case updateRequired
static func getCurrentState() -> RootViewControllerState {
@InjectService var accountManager: AccountManageable
diff --git a/kDriveActionExtension/ActionNavigationController.swift b/kDriveActionExtension/ActionNavigationController.swift
index d705249ed..0cd4090d1 100644
--- a/kDriveActionExtension/ActionNavigationController.swift
+++ b/kDriveActionExtension/ActionNavigationController.swift
@@ -21,10 +21,11 @@ import InfomaniakDI
import InfomaniakLogin
import kDriveCore
import UIKit
+import VersionChecker
final class ActionNavigationController: TitleSizeAdjustingNavigationController {
/// Making sure the DI is registered at a very early stage of the app launch.
- private let dependencyInjectionHook = EarlyDIHook()
+ private let dependencyInjectionHook = EarlyDIHook(context: .actionExtension)
@LazyInjectService var accountManager: AccountManageable
@@ -44,6 +45,10 @@ final class ActionNavigationController: TitleSizeAdjustingNavigationController {
saveFileViewController.itemProviders = itemProviders
viewControllers = [saveFileViewController]
+
+ Task {
+ try? await checkAppVersion()
+ }
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
@@ -52,4 +57,15 @@ final class ActionNavigationController: TitleSizeAdjustingNavigationController {
}
extensionContext.completeRequest(returningItems: extensionContext.inputItems, completionHandler: nil)
}
+
+ private func checkAppVersion() async throws {
+ guard try await VersionChecker.standard.checkAppVersionStatus() == .updateIsRequired else { return }
+ Task { @MainActor in
+ let updateRequiredViewController = DriveUpdateRequiredViewController()
+ updateRequiredViewController.dismissHandler = { [weak self] in
+ self?.dismiss(animated: true)
+ }
+ viewControllers = [updateRequiredViewController]
+ }
+ }
}
diff --git a/kDriveCore/DI/FactoryService.swift b/kDriveCore/DI/FactoryService.swift
index f6465660c..cf10e9340 100644
--- a/kDriveCore/DI/FactoryService.swift
+++ b/kDriveCore/DI/FactoryService.swift
@@ -29,6 +29,7 @@ public typealias FactoryWithIdentifier = (factory: Factory, identifier: String?)
private let appGroupName = "group.com.infomaniak.drive"
private let realmRootPath = "drives"
+private let loginConfig = InfomaniakLogin.Config(clientId: "9473D73C-C20F-4971-9E10-D957C563FA68", accessType: nil)
/// Something that setups the service factories
public enum FactoryService {
@@ -42,9 +43,7 @@ public enum FactoryService {
private static var networkingServices: [Factory] {
let services = [
Factory(type: InfomaniakNetworkLogin.self) { _, _ in
- let clientId = "9473D73C-C20F-4971-9E10-D957C563FA68"
- let redirectUri = "com.infomaniak.drive://oauth2redirect"
- return InfomaniakNetworkLogin(clientId: clientId, redirectUri: redirectUri)
+ return InfomaniakNetworkLogin(config: loginConfig)
},
Factory(type: InfomaniakNetworkLoginable.self) { _, resolver in
try resolver.resolve(type: InfomaniakNetworkLogin.self,
@@ -53,7 +52,7 @@ public enum FactoryService {
resolver: resolver)
},
Factory(type: InfomaniakLoginable.self) { _, _ in
- InfomaniakLogin(clientId: DriveApiFetcher.clientId)
+ InfomaniakLogin(config: loginConfig)
},
Factory(type: InfomaniakTokenable.self) { _, resolver in
try resolver.resolve(type: InfomaniakLoginable.self,
@@ -133,6 +132,9 @@ public enum FactoryService {
},
Factory(type: PhotoLibrarySavable.self) { _, _ in
PhotoLibrarySaver()
+ },
+ Factory(type: BackgroundTasksServiceable.self) { _, _ in
+ BackgroundTasksService()
}
]
return services
diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift
index 40c982fe5..a73afec89 100644
--- a/kDriveCore/Data/Api/DriveApiFetcher.swift
+++ b/kDriveCore/Data/Api/DriveApiFetcher.swift
@@ -52,8 +52,6 @@ public class AuthenticatedImageRequestModifier: ImageDownloadRequestModifier {
}
public class DriveApiFetcher: ApiFetcher {
- public static let clientId = "9473D73C-C20F-4971-9E10-D957C563FA68"
-
@LazyInjectService var accountManager: AccountManageable
@LazyInjectService var tokenable: InfomaniakTokenable
@@ -467,6 +465,7 @@ public class DriveApiFetcher: ApiFetcher {
class SyncedAuthenticator: OAuthAuthenticator {
@LazyInjectService var accountManager: AccountManageable
@LazyInjectService var tokenable: InfomaniakTokenable
+ @LazyInjectService var appContextService: AppContextServiceable
override func refresh(
_ credential: OAuthAuthenticator.Credential,
@@ -487,21 +486,29 @@ class SyncedAuthenticator: OAuthAuthenticator {
return
}
- // Maybe someone else refreshed our token
- self.accountManager.reloadTokensAndAccounts()
- if let token = self.accountManager.getTokenForUserId(credential.userId),
- token.expirationDate > credential.expirationDate {
- let message = "Refreshing token - Success with local"
- SentryDebug.addBreadcrumb(message: message, category: .apiToken, level: .info, metadata: metadata)
-
- completion(.success(token))
- return
+ if let storedToken = self.accountManager.getTokenForUserId(credential.userId) {
+ // Someone else refreshed our token and we already have an infinite token
+ if storedToken.expirationDate == nil && credential.expirationDate != nil {
+ let message = "Refreshing token - Success with local (infinite)"
+ SentryDebug.addBreadcrumb(message: message, category: .apiToken, level: .info, metadata: metadata)
+ completion(.success(storedToken))
+ return
+ }
+ // Someone else refreshed our token and we don't have an infinite token
+ if let storedTokenExpirationDate = storedToken.expirationDate,
+ let tokenExpirationDate = credential.expirationDate,
+ tokenExpirationDate > storedTokenExpirationDate {
+ let message = "Refreshing token - Success with local"
+ SentryDebug.addBreadcrumb(message: message, category: .apiToken, level: .info, metadata: metadata)
+ completion(.success(storedToken))
+ return
+ }
}
let group = DispatchGroup()
group.enter()
var taskIdentifier: UIBackgroundTaskIdentifier = .invalid
- if !Bundle.main.isExtension {
+ if !self.appContextService.isExtension {
// It is absolutely necessary that the app stays awake while we refresh the token
taskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Refresh token") {
let message = "Refreshing token failed - Background task expired"
diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift
index d97382116..017900cd1 100644
--- a/kDriveCore/Data/Cache/AccountManager.swift
+++ b/kDriveCore/Data/Cache/AccountManager.swift
@@ -33,19 +33,6 @@ public protocol AccountManagerDelegate: AnyObject {
}
public extension InfomaniakLogin {
- static func apiToken(username: String, applicationPassword: String) async throws -> ApiToken {
- try await withCheckedThrowingContinuation { continuation in
- @InjectService var tokenable: InfomaniakTokenable
- tokenable.getApiToken(username: username, applicationPassword: applicationPassword) { token, error in
- if let token {
- continuation.resume(returning: token)
- } else {
- continuation.resume(throwing: error ?? DriveError.unknownError)
- }
- }
- }
- }
-
static func apiToken(using code: String, codeVerifier: String) async throws -> ApiToken {
try await withCheckedThrowingContinuation { continuation in
@InjectService var tokenable: InfomaniakTokenable
@@ -270,11 +257,13 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable {
}
public func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) {
+ SentryDebug.logTokenMigration(newToken: newToken, oldToken: oldToken)
updateToken(newToken: newToken, oldToken: oldToken)
}
public func didFailRefreshToken(_ token: ApiToken) {
- let context = ["User id": token.userId, "Expiration date": token.expirationDate.timeIntervalSince1970] as [String: Any]
+ let context = ["User id": token.userId,
+ "Expiration date": token.expirationDate?.timeIntervalSince1970 ?? "Infinite"] as [String: Any]
SentryDebug.capture(message: "Failed refreshing token", context: context, contextKey: "Token Infos")
tokens.removeAll { $0.userId == token.userId }
diff --git a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift
index 7244844b0..a49585b40 100644
--- a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift
+++ b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift
@@ -26,6 +26,7 @@ public class DownloadArchiveOperation: Operation {
// MARK: - Attributes
@LazyInjectService var accountManager: AccountManageable
+ @LazyInjectService var appContextService: AppContextServiceable
private let archiveId: String
private let driveFileManager: DriveFileManager
@@ -85,7 +86,7 @@ public class DownloadArchiveOperation: Operation {
return
}
- if !Bundle.main.isExtension {
+ if !appContextService.isExtension {
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "File Archive Downloader") {
DownloadQueue.instance.suspendAllOperations()
// We don't support task rescheduling for archive download but still need to pass error to differentiate from user
diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift
index 1c158377a..4293d1491 100644
--- a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift
+++ b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift
@@ -36,6 +36,7 @@ public class DownloadOperation: Operation, DownloadOperationable {
@LazyInjectService var accountManager: AccountManageable
@LazyInjectService var downloadManager: BackgroundDownloadSessionManager
+ @LazyInjectService var appContextService: AppContextServiceable
private let fileManager = FileManager.default
private let driveFileManager: DriveFileManager
@@ -116,7 +117,7 @@ public class DownloadOperation: Operation, DownloadOperationable {
return
}
- if !Bundle.main.isExtension {
+ if !appContextService.isExtension {
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "File Downloader") {
DownloadQueue.instance.suspendAllOperations()
DDLogInfo("[DownloadOperation] Background task expired")
diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift
index 2823e9b85..6a61752c6 100644
--- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift
+++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift
@@ -58,6 +58,7 @@ public final class DownloadQueue {
// MARK: - Attributes
@LazyInjectService var accountManager: AccountManageable
+ @LazyInjectService var appContextService: AppContextServiceable
public static let instance = DownloadQueue()
public static let backgroundIdentifier = "com.infomaniak.background.download"
@@ -90,7 +91,7 @@ public final class DownloadQueue {
)
private var bestSession: FileDownloadSession {
- if Bundle.main.isExtension {
+ if appContextService.isExtension {
@InjectService var backgroundDownloadSessionManager: BackgroundDownloadSessionManager
return backgroundDownloadSessionManager
} else {
@@ -123,7 +124,7 @@ public final class DownloadQueue {
self.dispatchQueue.async {
self.operationsInQueue.removeValue(forKey: fileId)
self.publishFileDownloaded(fileId: fileId, error: operation.error)
- OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: self.operationsInQueue.isEmpty)
+ OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty)
}
}
self.operationQueue.addOperation(operation)
@@ -149,7 +150,7 @@ public final class DownloadQueue {
self.dispatchQueue.async {
self.archiveOperationsInQueue.removeValue(forKey: archiveId)
self.publishArchiveDownloaded(archiveId: archiveId, archiveUrl: operation.archiveUrl, error: operation.error)
- OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: self.operationsInQueue.isEmpty)
+ OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty)
}
}
self.operationQueue.addOperation(operation)
@@ -179,7 +180,7 @@ public final class DownloadQueue {
operation.completionBlock = {
self.dispatchQueue.async {
self.operationsInQueue.removeValue(forKey: fileId)
- OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: self.operationsInQueue.isEmpty)
+ OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty)
completion(operation.error)
}
}
@@ -220,25 +221,25 @@ public final class DownloadQueue {
}
private func publishFileDownloaded(fileId: Int, error: DriveError?) {
- observations.didDownloadFile.values.forEach { closure in
+ for closure in observations.didDownloadFile.values {
closure(fileId, error)
}
}
func publishProgress(_ progress: Double, for fileId: Int) {
- observations.didChangeProgress.values.forEach { closure in
+ for closure in observations.didChangeProgress.values {
closure(fileId, progress)
}
}
private func publishArchiveDownloaded(archiveId: String, archiveUrl: URL?, error: DriveError?) {
- observations.didDownloadArchive.values.forEach { closure in
+ for closure in observations.didDownloadArchive.values {
closure(archiveId, archiveUrl, error)
}
}
func publishProgress(_ progress: Double, for archiveId: String) {
- observations.didChangeArchiveProgress.values.forEach { closure in
+ for closure in observations.didChangeArchiveProgress.values {
closure(archiveId, progress)
}
}
diff --git a/kDriveCore/Data/Models/Upload/UploadFile.swift b/kDriveCore/Data/Models/Upload/UploadFile.swift
index 1e86833ba..cf95dfcd5 100644
--- a/kDriveCore/Data/Models/Upload/UploadFile.swift
+++ b/kDriveCore/Data/Models/Upload/UploadFile.swift
@@ -171,6 +171,18 @@ public final class UploadFile: Object, UploadFilable {
// primary key is set as default value
}
+ /// Init method of the UploadFile object
+ /// - Parameters:
+ /// - parentDirectoryId: Parent directory.
+ /// - userId: linked userId.
+ /// - driveId: linked driveId.
+ /// - fileProviderItemIdentifier: optional identifier from fileProvider.
+ /// - url: the url of the file to be uploaded.
+ /// - name: the name to be used.
+ /// - conflictOption: How to resolve an upload conflict with the API.
+ /// - shouldRemoveAfterUpload: remove after the upload in finished.
+ /// - initiatedFromFileManager: true if created from file manager.
+ /// - priority: The relative priority of the upload within the upload queue, defaults to `.high`.
public init(
parentDirectoryId: Int,
userId: Int,
@@ -181,7 +193,7 @@ public final class UploadFile: Object, UploadFilable {
conflictOption: ConflictOption = .rename,
shouldRemoveAfterUpload: Bool = true,
initiatedFromFileManager: Bool = false,
- priority: Operation.QueuePriority = .normal
+ priority: Operation.QueuePriority = .high
) {
super.init()
// primary key is set as default value
@@ -311,8 +323,6 @@ public extension UploadFile {
error = nil
// Reset retry count to default
maxRetryCount = UploadFile.defaultMaxRetryCount
- // Assign the UploadFile to the main app, not the FileManager extension
- initiatedFromFileManager = false
}
}
diff --git a/kDriveCore/Data/UploadQueue/Chunk/RangeProviderGuts.swift b/kDriveCore/Data/UploadQueue/Chunk/RangeProviderGuts.swift
index d120c47cb..6a7664da2 100644
--- a/kDriveCore/Data/UploadQueue/Chunk/RangeProviderGuts.swift
+++ b/kDriveCore/Data/UploadQueue/Chunk/RangeProviderGuts.swift
@@ -17,6 +17,7 @@
*/
import Foundation
+import InfomaniakDI
/// The internal methods of RangeProviderGuts, made testable
public protocol RangeProviderGutsable {
@@ -45,6 +46,8 @@ public protocol RangeProviderGutsable {
/// Subdivided **RangeProvider**, so it is easier to test
public struct RangeProviderGuts: RangeProviderGutsable {
+ @LazyInjectService private var appContextService: AppContextServiceable
+
/// The URL of the local file to scan
public let fileURL: URL
@@ -107,7 +110,7 @@ public struct RangeProviderGuts: RangeProviderGutsable {
public func preferredChunkSize(for fileSize: UInt64) -> UInt64 {
// In extension to reduce memory footprint, we reduce drastically chunk size
- guard !Bundle.main.isExtension else {
+ guard !appContextService.isExtension else {
let capChunkSize = min(fileSize, RangeProvider.APIConstants.chunkMinSize)
return capChunkSize
}
diff --git a/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift b/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift
index be1c90bd8..8124b70bc 100644
--- a/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift
+++ b/kDriveCore/Data/UploadQueue/Operation/UploadOperation.swift
@@ -245,7 +245,7 @@ public final class UploadOperation: AsynchronousOperation, UploadOperationable {
Log.uploadOperation("call finish ufid:\(uploadFileId)")
// Make sure we stop the expiring activity
- self.expiringActivity?.end()
+ self.expiringActivity?.endAll()
// Make sure we stop all the network requests (if any)
self.cancelAllUploadRequests()
diff --git a/kDriveCore/Data/UploadQueue/Queue/UploadParallelismHeuristic.swift b/kDriveCore/Data/UploadQueue/Queue/UploadParallelismHeuristic.swift
new file mode 100644
index 000000000..5affeb1e8
--- /dev/null
+++ b/kDriveCore/Data/UploadQueue/Queue/UploadParallelismHeuristic.swift
@@ -0,0 +1,108 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2023 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakDI
+
+/// Delegate protocol of UploadParallelismHeuristic
+protocol ParallelismHeuristicDelegate: AnyObject {
+ /// This method is called with a new parallelism to apply each time to the uploadQueue
+ /// - Parameter value: The new parallelism value to use
+ func parallelismShouldChange(value: Int)
+}
+
+/// Something to maintain a coherent parallelism value for the UploadQueue
+///
+/// Value can change depending on many factors, including thermal state battery or extension mode.
+/// Scaling is achieved given the number of active cores available.
+final class UploadParallelismHeuristic {
+ /// With 2 Operations max, and a chuck of 1MiB max, the UploadQueue can spike to max 4MiB memory usage.
+ private static let reducedParallelism = 2
+
+ @LazyInjectService private var appContextService: AppContextServiceable
+
+ private weak var delegate: ParallelismHeuristicDelegate?
+
+ init(delegate: ParallelismHeuristicDelegate) {
+ self.delegate = delegate
+
+ // Update on thermal change
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(computeParallelism),
+ name: ProcessInfo.thermalStateDidChangeNotification,
+ object: nil
+ )
+
+ // Update on low power mode
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(computeParallelism),
+ name: NSNotification.Name.NSProcessInfoPowerStateDidChange,
+ object: nil
+ )
+
+ // Update the value a first time
+ computeParallelism()
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self, name: ProcessInfo.thermalStateDidChangeNotification, object: nil)
+ NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NSProcessInfoPowerStateDidChange, object: nil)
+ }
+
+ @objc private func computeParallelism() {
+ let processInfo = ProcessInfo.processInfo
+
+ // If the device is too hot we cool down now
+ let thermalState = processInfo.thermalState
+ guard thermalState != .critical else {
+ currentParallelism = Self.reducedParallelism
+ return
+ }
+
+ // In low power mode, we reduce parallelism
+ guard !processInfo.isLowPowerModeEnabled else {
+ currentParallelism = Self.reducedParallelism
+ return
+ }
+
+ // In extension, to reduce memory footprint, we reduce drastically parallelism
+ guard !appContextService.isExtension else {
+ currentParallelism = Self.reducedParallelism
+ return
+ }
+
+ // Scaling with the number of activeProcessor
+ let parallelism = max(4, processInfo.activeProcessorCount)
+
+ // Beginning with .serious state, we start reducing the load on the system
+ guard thermalState != .serious else {
+ currentParallelism = max(Self.reducedParallelism, parallelism / 2)
+ return
+ }
+
+ currentParallelism = parallelism
+ }
+
+ public private(set) var currentParallelism = 0 {
+ didSet {
+ delegate?.parallelismShouldChange(value: currentParallelism)
+ }
+ }
+}
diff --git a/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift b/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift
index 68d0a3c43..93752b193 100644
--- a/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift
+++ b/kDriveCore/Data/UploadQueue/Queue/UploadQueue+Queue.swift
@@ -28,7 +28,14 @@ public protocol UploadQueueable {
/// Read database to enqueue all non finished upload tasks.
func rebuildUploadQueueFromObjectsInRealm(_ caller: StaticString)
- func saveToRealmAndAddToQueue(uploadFile: UploadFile, itemIdentifier: NSFileProviderItemIdentifier?) -> UploadOperationable?
+ /// Save an UploadFile in base and optionally enqueue the upload in main app
+ /// - Parameters:
+ /// - uploadFile: The upload file to be processed
+ /// - itemIdentifier: Optional item identifier
+ /// - addToQueue: Should we schedule the upload as well ?
+ /// - Returns: An UploadOperation if any
+ func saveToRealm(_ uploadFile: UploadFile, itemIdentifier: NSFileProviderItemIdentifier?, addToQueue: Bool)
+ -> UploadOperationable?
func suspendAllOperations()
@@ -82,18 +89,28 @@ extension UploadQueue: UploadQueueable {
public func getOperation(forUploadFileId uploadFileId: String) -> UploadOperationable? {
Log.uploadQueue("getOperation ufid:\(uploadFileId)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return nil
+ }
+
let operation = operation(uploadFileId: uploadFileId)
return operation
}
public func rebuildUploadQueueFromObjectsInRealm(_ caller: StaticString = #function) {
Log.uploadQueue("rebuildUploadQueueFromObjectsInRealm caller:\(caller)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
concurrentQueue.sync {
var uploadingFileIds = [String]()
try? self.transactionWithUploadRealm { realm in
- // Not uploaded yet, And can retry, And not initiated from the Files.app
+ // Not uploaded yet, And can retry.
let uploadingFiles = realm.objects(UploadFile.self)
- .filter("uploadDate = nil AND maxRetryCount > 0 AND initiatedFromFileManager = false")
+ .filter("uploadDate = nil AND maxRetryCount > 0")
.sorted(byKeyPath: "taskCreationDate")
uploadingFileIds = uploadingFiles.map(\.id)
Log.uploadQueue("rebuildUploadQueueFromObjectsInRealm uploads to restart:\(uploadingFileIds.count)")
@@ -104,10 +121,10 @@ extension UploadQueue: UploadQueueable {
for batch in batches {
Log.uploadQueue("rebuildUploadQueueFromObjectsInRealm in batch")
try? self.transactionWithUploadRealm { realm in
- batch.forEach { fileId in
+ for fileId in batch {
guard let file = realm.object(ofType: UploadFile.self, forPrimaryKey: fileId),
!file.isInvalidated else {
- return
+ continue
}
self.addToQueueIfNecessary(uploadFile: file, using: realm)
}
@@ -120,9 +137,13 @@ extension UploadQueue: UploadQueueable {
}
@discardableResult
- public func saveToRealmAndAddToQueue(uploadFile: UploadFile,
- itemIdentifier: NSFileProviderItemIdentifier? = nil) -> UploadOperationable? {
- Log.uploadQueue("saveToRealmAndAddToQueue ufid:\(uploadFile.id)")
+ public func saveToRealm(_ uploadFile: UploadFile,
+ itemIdentifier: NSFileProviderItemIdentifier? = nil,
+ addToQueue: Bool = true) -> UploadOperationable? {
+ let expiringActivity = ExpiringActivity()
+ expiringActivity.start()
+
+ Log.uploadQueue("saveToRealm addToQueue:\(addToQueue) ufid:\(uploadFile.id)")
assert(!uploadFile.isManagedByRealm, "we expect the file to be outside of realm at the moment")
@@ -146,32 +167,55 @@ extension UploadQueue: UploadQueueable {
Log.uploadQueue("did save ufid:\(uploadFile.id)")
}
- // Process adding a detached file to the uploadQueue
- var uploadOperation: UploadOperation?
- try? transactionWithUploadRealm { realm in
- uploadOperation = self.addToQueue(uploadFile: detachedFile, itemIdentifier: itemIdentifier, using: realm)
+ guard addToQueue else {
+ expiringActivity.endAll()
+ return nil
+ }
+
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("addToQueue disabled in ShareExtension", level: .error)
+ return nil
}
+ // Process adding a detached file to the uploadQueue
+ let uploadOperation = self.addToQueue(uploadFile: detachedFile, itemIdentifier: itemIdentifier)
+ expiringActivity.endAll()
+
return uploadOperation
}
public func suspendAllOperations() {
Log.uploadQueue("suspendAllOperations")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
forceSuspendQueue = true
operationQueue.isSuspended = true
}
public func resumeAllOperations() {
Log.uploadQueue("resumeAllOperations")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
forceSuspendQueue = false
operationQueue.isSuspended = shouldSuspendQueue
}
public func rescheduleRunningOperations() {
Log.uploadQueue("rescheduleRunningOperations")
- operationQueue.operations.filter(\.isExecuting).forEach { operation in
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
+ for operation in operationQueue.operations.filter(\.isExecuting) {
guard let uploadOperation = operation as? UploadOperation else {
- return
+ continue
}
// Mark the operation as rescheduled
@@ -181,12 +225,22 @@ extension UploadQueue: UploadQueueable {
public func cancelRunningOperations() {
Log.uploadQueue("cancelRunningOperations")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
operationQueue.operations.filter(\.isExecuting).forEach { $0.cancel() }
}
@discardableResult
public func cancel(uploadFileId: String) -> Bool {
Log.uploadQueue("cancel uploadFileId:\(uploadFileId)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return false
+ }
+
var found = false
concurrentQueue.sync {
try? self.transactionWithUploadRealm { realm in
@@ -204,6 +258,11 @@ extension UploadQueue: UploadQueueable {
public func cancel(uploadFile: UploadFile) {
Log.uploadQueue("cancel UploadFile ufid:\(uploadFile.id)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
let uploadFileId = uploadFile.id
let userId = uploadFile.userId
let parentId = uploadFile.parentDirectoryId
@@ -240,6 +299,11 @@ extension UploadQueue: UploadQueueable {
public func cancelAllOperations(withParent parentId: Int, userId: Int, driveId: Int) {
Log.uploadQueue("cancelAllOperations parentId:\(parentId)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
concurrentQueue.async {
Log.uploadQueue("suspend queue")
self.suspendAllOperations()
@@ -266,7 +330,7 @@ extension UploadQueue: UploadQueueable {
let batches = uploadingFilesIds.chunks(ofCount: 100)
for fileIds in batches {
autoreleasepool {
- fileIds.forEach { id in
+ for id in fileIds {
// Cancel operation if any
if let operation = self.keyedUploadOperations.getObject(forKey: id) {
operation.cancel()
@@ -289,11 +353,16 @@ extension UploadQueue: UploadQueueable {
public func cleanNetworkAndLocalErrorsForAllOperations() {
Log.uploadQueue("cleanErrorsForAllOperations")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
concurrentQueue.sync {
try? self.transactionWithUploadRealm { realm in
- // UploadFile with an error, Or no more retry, Or is initiatedFromFileManager
+ // UploadFile with an error, Or no more retry.
let failedUploadFiles = realm.objects(UploadFile.self)
- .filter("_error != nil OR maxRetryCount <= 0 OR initiatedFromFileManager = true")
+ .filter("_error != nil OR maxRetryCount <= 0")
.filter { file in
guard let error = file.error else {
return false
@@ -304,7 +373,7 @@ extension UploadQueue: UploadQueueable {
Log.uploadQueue("will clean errors for uploads:\(failedUploadFiles.count)")
try? realm.safeWrite {
- failedUploadFiles.forEach { file in
+ for file in failedUploadFiles {
file.clearErrorsForRetry()
}
}
@@ -315,6 +384,11 @@ extension UploadQueue: UploadQueueable {
public func retry(_ uploadFileId: String) {
Log.uploadQueue("retry ufid:\(uploadFileId)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
concurrentQueue.async {
try? self.transactionWithUploadRealm { realm in
guard let file = realm.object(ofType: UploadFile.self, forPrimaryKey: uploadFileId), !file.isInvalidated else {
@@ -341,7 +415,7 @@ extension UploadQueue: UploadQueueable {
return
}
- self.addToQueue(uploadFile: file, using: realm)
+ self.addToQueue(uploadFile: file)
}
self.resumeAllOperations()
@@ -350,6 +424,11 @@ extension UploadQueue: UploadQueueable {
public func retryAllOperations(withParent parentId: Int, userId: Int, driveId: Int) {
Log.uploadQueue("retryAllOperations parentId:\(parentId)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
concurrentQueue.async {
let failedFileIds = self.getFailedFileIds(parentId: parentId, userId: userId, driveId: driveId)
let batches = failedFileIds.chunks(ofCount: 100)
@@ -380,7 +459,7 @@ extension UploadQueue: UploadQueueable {
using: realm)
Log.uploadQueue("uploading:\(uploadingFiles.count)")
let failedUploadFiles = uploadingFiles
- .filter("_error != nil OR maxRetryCount <= 0 OR initiatedFromFileManager = true")
+ .filter("_error != nil OR maxRetryCount <= 0")
failedFileIds = failedUploadFiles.map(\.id)
Log.uploadQueue("retying:\(failedFileIds.count)")
}
@@ -388,8 +467,13 @@ extension UploadQueue: UploadQueueable {
}
private func cancelAnyInBatch(_ batch: ArraySlice) {
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
try? transactionWithUploadRealm { realm in
- batch.forEach { uploadFileId in
+ for uploadFileId in batch {
// Cancel operation if any
if let operation = self.operation(uploadFileId: uploadFileId) {
operation.cancel()
@@ -399,7 +483,7 @@ extension UploadQueue: UploadQueueable {
// Clean errors in db file
guard let file = realm.object(ofType: UploadFile.self, forPrimaryKey: uploadFileId), !file.isInvalidated else {
Log.uploadQueue("file invalidated ufid:\(uploadFileId) at\(#line)")
- return
+ continue
}
try? realm.safeWrite {
file.clearErrorsForRetry()
@@ -409,11 +493,16 @@ extension UploadQueue: UploadQueueable {
}
private func enqueueAnyInBatch(_ batch: ArraySlice) {
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
try? transactionWithUploadRealm { realm in
- batch.forEach { uploadFileId in
+ for uploadFileId in batch {
guard let file = realm.object(ofType: UploadFile.self, forPrimaryKey: uploadFileId), !file.isInvalidated else {
Log.uploadQueue("file invalidated ufid:\(uploadFileId) at\(#line)")
- return
+ continue
}
self.addToQueueIfNecessary(uploadFile: file, using: realm)
@@ -423,6 +512,11 @@ extension UploadQueue: UploadQueueable {
private func operation(uploadFileId: String) -> UploadOperationable? {
Log.uploadQueue("operation fileId:\(uploadFileId)")
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return nil
+ }
+
guard let operation = keyedUploadOperations.getObject(forKey: uploadFileId),
!operation.isCancelled,
!operation.isFinished else {
@@ -433,6 +527,11 @@ extension UploadQueue: UploadQueueable {
private func addToQueueIfNecessary(uploadFile: UploadFile, itemIdentifier: NSFileProviderItemIdentifier? = nil,
using realm: Realm) {
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return
+ }
+
guard !uploadFile.isInvalidated else {
return
}
@@ -440,7 +539,7 @@ extension UploadQueue: UploadQueueable {
Log.uploadQueue("rebuildUploadQueueFromObjectsInRealm ufid:\(uploadFile.id)")
guard operation(uploadFileId: uploadFile.id) != nil else {
Log.uploadQueue("rebuildUploadQueueFromObjectsInRealm ADD ufid:\(uploadFile.id)")
- addToQueue(uploadFile: uploadFile, itemIdentifier: nil, using: realm)
+ addToQueue(uploadFile: uploadFile, itemIdentifier: nil)
return
}
@@ -449,8 +548,12 @@ extension UploadQueue: UploadQueueable {
@discardableResult
private func addToQueue(uploadFile: UploadFile,
- itemIdentifier: NSFileProviderItemIdentifier? = nil,
- using realm: Realm) -> UploadOperation? {
+ itemIdentifier: NSFileProviderItemIdentifier? = nil) -> UploadOperation? {
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("\(#function) disabled in ShareExtension", level: .error)
+ return nil
+ }
+
guard !uploadFile.isInvalidated,
uploadFile.maxRetryCount > 0,
keyedUploadOperations.getObject(forKey: uploadFile.id) == nil else {
@@ -479,7 +582,7 @@ extension UploadQueue: UploadQueueable {
publishFileUploaded(result: operation.result)
publishUploadCount(withParent: parentId, userId: userId, driveId: driveId)
- OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: keyedUploadOperations.isEmpty)
+ OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !keyedUploadOperations.isEmpty)
}
Log.uploadQueue("add operation :\(operation) ufid:\(uploadFileId)")
diff --git a/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift b/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift
index c7c1acd99..8c36750e3 100644
--- a/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift
+++ b/kDriveCore/Data/UploadQueue/Queue/UploadQueue.swift
@@ -22,9 +22,10 @@ import InfomaniakDI
import RealmSwift
import Sentry
-public final class UploadQueue {
+public final class UploadQueue: ParallelismHeuristicDelegate {
@LazyInjectService var accountManager: AccountManageable
@LazyInjectService var notificationHelper: NotificationsHelpable
+ @LazyInjectService var appContextService: AppContextServiceable
public static let backgroundBaseIdentifier = ".backgroundsession.upload"
public static var backgroundIdentifier: String {
@@ -44,6 +45,8 @@ public final class UploadQueue {
/// Something to track an operation for a File ID
let keyedUploadOperations = KeyedUploadOperationable()
+ var uploadParallelismHeuristic: UploadParallelismHeuristic?
+
public lazy var operationQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "kDrive upload queue"
@@ -51,7 +54,7 @@ public final class UploadQueue {
// In extension to reduce memory footprint, we reduce drastically parallelism
let parallelism: Int
- if Bundle.main.isExtension {
+ if appContextService.isExtension {
parallelism = 2 // With 2 Operations max, and a chuck of 1MiB max, the UploadQueue can spike to max 4MiB.
} else {
parallelism = max(4, ProcessInfo.processInfo.activeProcessorCount)
@@ -82,6 +85,11 @@ public final class UploadQueue {
/// Should suspend operation queue based on network status
var shouldSuspendQueue: Bool {
+ // Explicitly disable the upload queue from the share extension
+ guard appContextService.context != .shareExtension else {
+ return true
+ }
+
let status = ReachabilityListener.instance.currentStatus
return status == .offline || (status != .wifi && UserDefaults.shared.isWifiOnly)
}
@@ -96,8 +104,15 @@ public final class UploadQueue {
)
public init() {
+ guard appContextService.context != .shareExtension else {
+ Log.uploadQueue("UploadQueue disabled in ShareExtension", level: .error)
+ return
+ }
+
Log.uploadQueue("Starting up")
+ uploadParallelismHeuristic = UploadParallelismHeuristic(delegate: self)
+
concurrentQueue.async {
// Initialize operation queue with files from Realm, and make sure it restarts
self.rebuildUploadQueueFromObjectsInRealm()
@@ -146,4 +161,11 @@ public final class UploadQueue {
public func getUploadedFiles(using realm: Realm = DriveFileManager.constants.uploadsRealm) -> Results {
return realm.objects(UploadFile.self).filter(NSPredicate(format: "uploadDate != nil"))
}
+
+ // MARK: - ParallelismHeuristicDelegate
+
+ func parallelismShouldChange(value: Int) {
+ Log.uploadQueue("Upload queue new parallelism: \(value)", level: .info)
+ operationQueue.maxConcurrentOperationCount = value
+ }
}
diff --git a/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader+Scan.swift b/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader+Scan.swift
index d1b956308..fb2009d89 100644
--- a/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader+Scan.swift
+++ b/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader+Scan.swift
@@ -37,19 +37,31 @@ public extension PhotoLibraryUploader {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
- let typesPredicates = getAssetPredicates(forSettings: settings)
+ let typesPredicates = self.getAssetPredicates(forSettings: settings)
let datePredicate = getDatePredicate(with: settings)
let typePredicate = NSCompoundPredicate(orPredicateWithSubpredicates: typesPredicates)
options.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [datePredicate, typePredicate])
Log.photoLibraryUploader("Fetching new pictures/videos with predicate: \(options.predicate!.predicateFormat)")
- let assets = PHAsset.fetchAssets(with: options)
+ let assetsFetchResult = PHAsset.fetchAssets(with: options)
let syncDate = Date()
- addImageAssetsToUploadQueue(assets: assets, initial: settings.lastSync.timeIntervalSince1970 == 0, using: realm)
- updateLastSyncDate(syncDate, using: realm)
- newAssetsCount = assets.count
- Log.photoLibraryUploader("New assets count:\(newAssetsCount)")
+ do {
+ try addImageAssetsToUploadQueue(
+ assetsFetchResult: assetsFetchResult,
+ initial: settings.lastSync.timeIntervalSince1970 == 0,
+ using: realm
+ )
+
+ updateLastSyncDate(syncDate, using: realm)
+
+ newAssetsCount = assetsFetchResult.count
+ Log.photoLibraryUploader("New assets count:\(newAssetsCount)")
+ } catch ErrorDomain.importCancelledBySystem {
+ Log.photoLibraryUploader("System is requesting to stop", level: .error)
+ } catch {
+ Log.photoLibraryUploader("addImageAssetsToUploadQueue error:\(error)", level: .error)
+ }
}
return newAssetsCount
}
@@ -71,15 +83,28 @@ public extension PhotoLibraryUploader {
}
}
- private func addImageAssetsToUploadQueue(assets: PHFetchResult,
+ private func addImageAssetsToUploadQueue(assetsFetchResult: PHFetchResult,
initial: Bool,
- using realm: Realm = DriveFileManager.constants.uploadsRealm) {
+ using realm: Realm = DriveFileManager.constants.uploadsRealm) throws {
Log.photoLibraryUploader("addImageAssetsToUploadQueue")
+ let expiringActivity = ExpiringActivity(id: "addImageAssetsToUploadQueue:\(UUID().uuidString)", delegate: nil)
+ expiringActivity.start()
+ defer {
+ expiringActivity.endAll()
+ }
+
autoreleasepool {
var burstIdentifier: String?
var burstCount = 0
realm.beginWrite()
- assets.enumerateObjects { [self] asset, idx, stop in
+ assetsFetchResult.enumerateObjects { [self] asset, idx, stop in
+ guard !expiringActivity.shouldTerminate else {
+ Log.photoLibraryUploader("system is asking to terminate")
+ realm.cancelWrite()
+ stop.pointee = true
+ return
+ }
+
guard let settings else {
Log.photoLibraryUploader("no settings")
realm.cancelWrite()
@@ -111,8 +136,17 @@ public extension PhotoLibraryUploader {
burstCount: &burstCount
)
- let bestResourceSHA256: String? = asset.bestResourceSHA256
- Log.photoLibraryUploader("Asset hash:\(bestResourceSHA256)")
+ let bestResourceSHA256: String?
+ do {
+ bestResourceSHA256 = try asset.bestResourceSHA256
+ } catch {
+ // Error thrown while hashing a resource, we stop ASAP.
+ Log.photoLibraryUploader("Error while hashing:\(error) asset: \(asset.localIdentifier)", level: .error)
+ stop.pointee = true
+ return
+ }
+
+ Log.photoLibraryUploader("Asset hash:\(String(describing: bestResourceSHA256))")
// Check if picture uploaded before
guard !assetAlreadyUploaded(assetName: finalName,
@@ -125,7 +159,7 @@ public extension PhotoLibraryUploader {
let algorithmImportVersion = currentDiffAlgorithmVersion
- // New UploadFile to be uploaded
+ // New UploadFile to be uploaded. Priority is `.low`, first sync is `.normal`
let uploadFile = UploadFile(
parentDirectoryId: settings.parentDirectoryId,
userId: settings.userId,
@@ -135,7 +169,7 @@ public extension PhotoLibraryUploader {
bestResourceSHA256: bestResourceSHA256,
algorithmImportVersion: algorithmImportVersion,
conflictOption: .version,
- priority: initial ? .low : .high
+ priority: initial ? .low : .normal
)
// Lazy creation of sub folder if required in the upload file
@@ -147,7 +181,7 @@ public extension PhotoLibraryUploader {
realm.add(uploadFile, update: .modified)
// Batching writes
- if idx < assets.count - 1 && idx % 99 == 0 {
+ if idx < assetsFetchResult.count - 1 && idx % 99 == 0 {
Log.photoLibraryUploader("Commit assets batch up to :\(idx)")
// Commit write every 100 assets if it's not the last
try? realm.commitWrite()
@@ -160,6 +194,10 @@ public extension PhotoLibraryUploader {
}
try? realm.commitWrite()
}
+
+ guard !expiringActivity.shouldTerminate else {
+ throw ErrorDomain.importCancelledBySystem
+ }
}
private func getPhotoLibraryName(
diff --git a/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader.swift b/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader.swift
index 4b0b229cf..bacea5960 100644
--- a/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader.swift
+++ b/kDriveCore/Data/UploadQueue/Servicies/PhotoLibraryUploader.swift
@@ -33,6 +33,11 @@ public final class PhotoLibraryUploader {
/// Predicate to quickly narrow down on uploaded assets
static let uploadedAssetPredicate = NSPredicate(format: "rawType = %@ AND uploadDate != nil", "phAsset")
+ enum ErrorDomain: Error {
+ /// System is asking to terminate the operation
+ case importCancelledBySystem
+ }
+
var _settings: PhotoSyncSettings?
public var settings: PhotoSyncSettings? {
_settings
diff --git a/kDriveCore/Services/AppContextService.swift b/kDriveCore/Services/AppContextService.swift
new file mode 100644
index 000000000..20cd4d3f2
--- /dev/null
+++ b/kDriveCore/Services/AppContextService.swift
@@ -0,0 +1,59 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2023 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+
+/// All the ways some code can be executed within the __kDrive__ project
+public enum DriveAppContext {
+ /// Current execution context is the main app
+ case app
+
+ /// Current execution context is an action extension
+ case actionExtension
+
+ /// Current execution context is the file provider extension
+ case fileProviderExtension
+
+ /// Current execution context is the share extension
+ case shareExtension
+}
+
+/// Something that can provide the active execution context
+public protocol AppContextServiceable {
+ /// Get the current execution context
+ var context: DriveAppContext { get }
+
+ /// Shorthand to check if we are within the main app or any extension
+ var isExtension: Bool { get }
+}
+
+public struct AppContextService: AppContextServiceable {
+ public var context: DriveAppContext
+
+ public var isExtension: Bool {
+ guard context == .app else {
+ return true
+ }
+
+ return false
+ }
+
+ public init(context: DriveAppContext) {
+ self.context = context
+ }
+}
diff --git a/kDriveCore/Services/BackgroundTasksService.swift b/kDriveCore/Services/BackgroundTasksService.swift
new file mode 100644
index 000000000..528c0818d
--- /dev/null
+++ b/kDriveCore/Services/BackgroundTasksService.swift
@@ -0,0 +1,172 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2023 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import BackgroundTasks
+import CocoaLumberjackSwift
+import Foundation
+import InfomaniakDI
+import kDriveCore
+
+/* To debug background tasks:
+ Launch ->
+ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.infomaniak.background.refresh"]
+ OR
+ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"]
+
+ Force early termination ->
+ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.refresh"]
+ OR
+ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"]
+ */
+
+/// Service to ask the system to do some work in the background later.
+public protocol BackgroundTasksServiceable {
+ /// Ask the system to handle the app's background refresh
+ func registerBackgroundTasks()
+
+ /// Schedule next refresh with the system
+ func scheduleBackgroundRefresh()
+}
+
+struct BackgroundTasksService: BackgroundTasksServiceable {
+ private static let activityShouldTerminateMessage = "Notified activity should terminate"
+
+ @LazyInjectService private var scheduler: BGTaskScheduler
+ @LazyInjectService private var accountManager: AccountManageable
+ @LazyInjectService private var uploadQueue: UploadQueue
+ @LazyInjectService private var photoUploader: PhotoLibraryUploader
+
+ public init() {
+ // META: keep SonarCloud happy
+ }
+
+ public func registerBackgroundTasks() {
+ Log.backgroundTaskScheduling("registerBackgroundTasks")
+ registerBackgroundTask(identifier: Constants.backgroundRefreshIdentifier)
+ registerBackgroundTask(identifier: Constants.longBackgroundRefreshIdentifier)
+ }
+
+ public func buildBackgroundTask(_ task: BGTask, identifier: String) {
+ scheduleBackgroundRefresh()
+
+ handleBackgroundRefresh { _ in
+ Log.backgroundTaskScheduling("Task \(identifier) completed with SUCCESS")
+ task.setTaskCompleted(success: true)
+ }
+
+ task.expirationHandler = {
+ Log.backgroundTaskScheduling("Task \(identifier) EXPIRED", level: .error)
+ uploadQueue.suspendAllOperations()
+ uploadQueue.rescheduleRunningOperations()
+ task.setTaskCompleted(success: false)
+ }
+ }
+
+ func registerBackgroundTask(identifier: String) {
+ let registered = scheduler.register(
+ forTaskWithIdentifier: identifier,
+ using: nil
+ ) { task in
+ buildBackgroundTask(task, identifier: identifier)
+ }
+ Log.backgroundTaskScheduling("Task \(identifier) registered ? \(registered)")
+ }
+
+ func handleBackgroundRefresh(completion: @escaping (Bool) -> Void) {
+ let expiringActivity = ExpiringActivity(id: UUID().uuidString, delegate: nil)
+ expiringActivity.start()
+
+ Log.backgroundTaskScheduling("handleBackgroundRefresh")
+ // User installed the app but never logged in
+ if expiringActivity.shouldTerminate || accountManager.accounts.isEmpty {
+ Log.backgroundTaskScheduling(Self.activityShouldTerminateMessage, level: .error)
+ completion(false)
+ expiringActivity.endAll()
+ return
+ }
+
+ Log.backgroundTaskScheduling("Enqueue new pictures")
+ photoUploader.scheduleNewPicturesForUpload()
+
+ guard !expiringActivity.shouldTerminate else {
+ Log.backgroundTaskScheduling(Self.activityShouldTerminateMessage, level: .error)
+ completion(false)
+ expiringActivity.endAll()
+ return
+ }
+
+ Log.backgroundTaskScheduling("Clean errors for all uploads")
+ uploadQueue.cleanNetworkAndLocalErrorsForAllOperations()
+
+ guard !expiringActivity.shouldTerminate else {
+ Log.backgroundTaskScheduling(Self.activityShouldTerminateMessage, level: .error)
+ completion(false)
+ expiringActivity.endAll()
+ return
+ }
+
+ Log.backgroundTaskScheduling("Reload operations in queue")
+ uploadQueue.rebuildUploadQueueFromObjectsInRealm()
+
+ guard !expiringActivity.shouldTerminate else {
+ Log.backgroundTaskScheduling(Self.activityShouldTerminateMessage, level: .error)
+ completion(false)
+ expiringActivity.endAll()
+ return
+ }
+
+ Log.backgroundTaskScheduling("waitForCompletion")
+ uploadQueue.waitForCompletion {
+ Log.backgroundTaskScheduling("Background activity ended with success")
+ completion(true)
+ expiringActivity.endAll()
+ }
+ }
+
+ func scheduleBackgroundRefresh() {
+ Log.backgroundTaskScheduling("scheduleBackgroundRefresh")
+ // List pictures + upload files (+pictures) / photoKit
+ let backgroundRefreshRequest = BGAppRefreshTaskRequest(identifier: Constants.backgroundRefreshIdentifier)
+ #if DEBUG
+ // Required for debugging
+ backgroundRefreshRequest.earliestBeginDate = Date()
+ #else
+ backgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60)
+ #endif
+
+ // Upload files (+pictures) / photokit
+ let longBackgroundRefreshRequest = BGProcessingTaskRequest(identifier: Constants.longBackgroundRefreshIdentifier)
+ #if DEBUG
+ // Required for debugging
+ longBackgroundRefreshRequest.earliestBeginDate = Date()
+ #else
+ longBackgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60)
+ #endif
+ longBackgroundRefreshRequest.requiresNetworkConnectivity = true
+ longBackgroundRefreshRequest.requiresExternalPower = true
+ do {
+ try scheduler.submit(backgroundRefreshRequest)
+ Log.backgroundTaskScheduling("scheduled task: \(backgroundRefreshRequest)")
+ try scheduler.submit(longBackgroundRefreshRequest)
+ Log.backgroundTaskScheduling("scheduled task: \(longBackgroundRefreshRequest)")
+
+ } catch {
+ Log.backgroundTaskScheduling("Error scheduling background task: \(error)", level: .error)
+ }
+ }
+}
diff --git a/kDriveCore/UI/IKLargeButton.swift b/kDriveCore/UI/IKLargeButton.swift
index 507aadf1d..acaf0fe84 100644
--- a/kDriveCore/UI/IKLargeButton.swift
+++ b/kDriveCore/UI/IKLargeButton.swift
@@ -48,9 +48,9 @@ import UIKit
public var disabledBackgroundColor = KDriveResourcesAsset.buttonDisabledBackgroundColor.color
public struct Style: RawRepresentable {
- var titleFont: UIFont
- var titleColor: UIColor
- var backgroundColor: UIColor
+ public var titleFont: UIFont
+ public var titleColor: UIColor
+ public var backgroundColor: UIColor
public static let primaryButton = Style(
titleFont: UIFont.systemFont(ofSize: UIFontMetrics.default.scaledValue(for: 16), weight: .medium),
diff --git a/kDriveCore/UI/UIConstants.swift b/kDriveCore/UI/UIConstants.swift
index 260edf705..375c4378f 100644
--- a/kDriveCore/UI/UIConstants.swift
+++ b/kDriveCore/UI/UIConstants.swift
@@ -24,6 +24,13 @@ import SnackBar
import UIKit
public enum UIConstants {
+ private static let style: SnackBarStyle = {
+ var style = SnackBarStyle.infomaniakStyle
+ style.anchor = 20.0
+ style.maxWidth = 600.0
+ return style
+ }()
+
public static let inputCornerRadius = 2.0
public static let imageCornerRadius = 3.0
public static let cornerRadius = 6.0
@@ -41,9 +48,12 @@ public enum UIConstants {
@discardableResult
@MainActor
- public static func showSnackBar(message: String, duration: SnackBar.Duration = .lengthLong,
+ public static func showSnackBar(message: String,
+ duration: SnackBar.Duration = .lengthLong,
action: IKSnackBar.Action? = nil) -> IKSnackBar? {
- let snackbar = IKSnackBar.make(message: message, duration: duration)
+ let snackbar = IKSnackBar.make(message: message,
+ duration: duration,
+ style: style)
if let action {
snackbar?.setAction(action).show()
} else {
diff --git a/kDriveCore/Utils/AbstractLog+Category.swift b/kDriveCore/Utils/AbstractLog+Category.swift
index d8c1fe3c5..7cafd1bed 100644
--- a/kDriveCore/Utils/AbstractLog+Category.swift
+++ b/kDriveCore/Utils/AbstractLog+Category.swift
@@ -90,13 +90,13 @@ public enum Log {
///
/// In system console, visualize them with `subsystem:com.infomaniak.drive category:BGTaskScheduling`
///
- public static func bgTaskScheduling(_ message: @autoclosure () -> Any,
- level: AbstractLogLevel = .debug,
- context: Int = 0,
- file: StaticString = #file,
- function: StaticString = #function,
- line: UInt = #line,
- tag: Any? = nil) {
+ public static func backgroundTaskScheduling(_ message: @autoclosure () -> Any,
+ level: AbstractLogLevel = .debug,
+ context: Int = 0,
+ file: StaticString = #file,
+ function: StaticString = #function,
+ line: UInt = #line,
+ tag: Any? = nil) {
let category = "BGTaskScheduling"
defaultLogHandler(message(),
category: category,
diff --git a/kDriveCore/Utils/Constants.swift b/kDriveCore/Utils/Constants.swift
index 0752c9a67..3d2d808f9 100644
--- a/kDriveCore/Utils/Constants.swift
+++ b/kDriveCore/Utils/Constants.swift
@@ -30,6 +30,7 @@ public enum PhotoLibraryImport: Int {
}
public struct URLConstants {
+ public static let kDriveRedirection = URLConstants(urlString: "https://kdrive.infomaniak.com/app/drive")
public static let kDriveWeb = URLConstants(urlString: "https://kdrive.infomaniak.com")
public static let signUp = URLConstants(urlString: "https://welcome.infomaniak.com/signup/ikdrive/steps")
public static let shop = URLConstants(urlString: "https://shop.infomaniak.com/order/drive")
diff --git a/kDriveCore/Utils/ExpiringActivity.swift b/kDriveCore/Utils/ExpiringActivity.swift
index fa3d61e22..acf18e603 100644
--- a/kDriveCore/Utils/ExpiringActivity.swift
+++ b/kDriveCore/Utils/ExpiringActivity.swift
@@ -19,71 +19,107 @@
import Foundation
import InfomaniakCore
+// TODO: User core 7.0.0 version ASAP
/// Delegation mechanism to notify the end of an `ExpiringActivity`
public protocol ExpiringActivityDelegate: AnyObject {
/// Called when the system is requiring us to terminate an expiring activity
func backgroundActivityExpiring()
}
+/// Something that can perform arbitrary short background tasks, with a super simple API.
public protocol ExpiringActivityable {
+ /// Common init method
+ /// - Parameters:
+ /// - id: Something to identify the background activity in debug
+ /// - qos: QoS used by the underlying queues
+ /// - delegate: The delegate to notify we should terminate
+ init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?)
+
+ /// init method
+ /// - Parameters:
+ /// - id: Something to identify the background activity in debug
+ /// - delegate: The delegate to notify we should terminate
init(id: String, delegate: ExpiringActivityDelegate?)
/// Register with the system an expiring activity
func start()
- /// Terminate the expiring activity if needed.
- func end()
+ /// Terminate all the expiring activities
+ func endAll()
+
+ /// True if the system asked to stop the background activity
+ var shouldTerminate: Bool { get }
}
public final class ExpiringActivity: ExpiringActivityable {
- /// Keep track of the locks on blocks
- private var locks = [TolerantDispatchGroup]()
+ private let qos: DispatchQoS
+
+ private let queue: DispatchQueue
- /// For thread safety
- private let queue = DispatchQueue(label: "com.infomaniak.ExpiringActivity.sync")
+ private let processInfo = ProcessInfo.processInfo
+
+ var locks = [TolerantDispatchGroup]()
- /// Something to identify the background activity in debug
let id: String
- /// The delegate to notify we should terminate
+ public var shouldTerminate = false
+
weak var delegate: ExpiringActivityDelegate?
// MARK: Lifecycle
- public init(id: String, delegate: ExpiringActivityDelegate?) {
+ public init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?) {
self.id = id
+ self.qos = qos
self.delegate = delegate
+ queue = DispatchQueue(label: "com.infomaniak.ExpiringActivity.sync", qos: qos)
+ }
+
+ public convenience init(id: String = "\(#function)-\(UUID().uuidString)", delegate: ExpiringActivityDelegate? = nil) {
+ self.init(id: id, qos: .userInitiated, delegate: delegate)
}
deinit {
queue.sync {
- assert(locks.isEmpty, "please make sure to balance 'start()' and 'end()' before releasing this object")
+ assert(locks.isEmpty, "please make sure to call 'endAll()' once explicitly before releasing this object")
}
}
public func start() {
- let group = TolerantDispatchGroup()
+ let group = TolerantDispatchGroup(qos: qos)
queue.sync {
self.locks.append(group)
}
+ #if os(macOS)
+ // We block a non cooperative queue that matches current QoS
+ DispatchQueue.global(qos: qos.qosClass).async {
+ self.processInfo.performActivity(options: .suddenTerminationDisabled, reason: self.id) {
+ // No expiration handler as we are running on macOS
+ group.enter()
+ group.wait()
+ }
+ }
+ #else
// Make sure to not lock an unexpected thread that would deinit()
- ProcessInfo.processInfo.performExpiringActivity(withReason: id) { [weak self] shouldTerminate in
+ processInfo.performExpiringActivity(withReason: id) { [weak self] shouldTerminate in
guard let self else {
return
}
if shouldTerminate {
+ self.shouldTerminate = true
delegate?.backgroundActivityExpiring()
}
group.enter()
group.wait()
}
+ #endif
}
- public func end() {
+ public func endAll() {
queue.sync {
// Release locks, oldest first
for group in locks.reversed() {
diff --git a/kDriveCore/Utils/Files/FileImportHelper+Upload.swift b/kDriveCore/Utils/Files/FileImportHelper+Upload.swift
index 1c3db97fa..7002951f0 100644
--- a/kDriveCore/Utils/Files/FileImportHelper+Upload.swift
+++ b/kDriveCore/Utils/Files/FileImportHelper+Upload.swift
@@ -26,11 +26,14 @@ import RealmSwift
import VisionKit
public extension FileImportHelper {
- func upload(files: [ImportedFile], in directory: File, drive: Drive) async throws {
+ func saveForUpload(_ files: [ImportedFile], in directory: File, drive: Drive, addToQueue: Bool) async throws {
guard directory.capabilities.canUpload else {
throw ImportError.accessDenied
}
+ let expiringActivity = ExpiringActivity()
+ expiringActivity.start()
+
let parentDirectoryId = directory.id
let userId = drive.userId
let driveId = drive.id
@@ -43,8 +46,11 @@ public extension FileImportHelper {
url: file.path,
name: file.name
)
- self.uploadQueue.saveToRealmAndAddToQueue(uploadFile: uploadFile)
+
+ self.uploadQueue.saveToRealm(uploadFile, addToQueue: addToQueue)
}
+
+ expiringActivity.endAll()
}
func upload(
@@ -133,7 +139,7 @@ public extension FileImportHelper {
url: targetURL,
name: name
)
- uploadQueue.saveToRealmAndAddToQueue(uploadFile: newFile)
+ uploadQueue.saveToRealm(newFile)
}
}
diff --git a/kDriveCore/Utils/Files/FileImportHelper.swift b/kDriveCore/Utils/Files/FileImportHelper.swift
index 76d9afa97..87410a566 100644
--- a/kDriveCore/Utils/Files/FileImportHelper.swift
+++ b/kDriveCore/Utils/Files/FileImportHelper.swift
@@ -67,8 +67,12 @@ public enum ImportError: LocalizedError {
}
public final class FileImportHelper {
+ /// Shorthand for default FileManager
+ private let fileManager = FileManager.default
+
@LazyInjectService var pathProvider: AppGroupPathProvidable
@LazyInjectService var uploadQueue: UploadQueue
+ @LazyInjectService var appContextService: AppContextServiceable
let imageCompression = 0.8
@@ -102,7 +106,7 @@ public final class FileImportHelper {
Task {
do {
- let results: [Result?] = await assetIdentifiers.concurrentMap { assetIdentifier in
+ let results: [Result?] = try await assetIdentifiers.concurrentMap { assetIdentifier in
defer {
progress.completedUnitCount += 1
}
@@ -117,12 +121,27 @@ public final class FileImportHelper {
}
let uti = UTI(filenameExtension: url.pathExtension)
- var name = url.lastPathComponent
- if let uti, let originalName = asset.getFilename(uti: uti) {
- name = originalName
+ var fileName = url.lastPathComponent
+ if let uti,
+ let originalName = asset.getFilename(uti: uti) {
+ fileName = originalName
+ }
+
+ var finalUrl: URL
+ if self.appContextService.isExtension {
+ // In extension, we need to copy files to a path within appGroup to be able to upload from the main app.
+ let appGroupURL = try URL.appGroupUniqueFolderURL()
+
+ // Get import URL
+ let appGroupFileURL = appGroupURL.appendingPathComponent(fileName)
+ try self.fileManager.copyItem(atPath: url.path, toPath: appGroupFileURL.path)
+ finalUrl = appGroupFileURL
+ } else {
+ // Path obtained within the main app are stable, and will stay accessible.
+ finalUrl = url
}
- let importedFile = ImportedFile(name: name, path: url, uti: uti ?? .data)
+ let importedFile = ImportedFile(name: fileName, path: finalUrl, uti: uti ?? .data)
return .success(importedFile)
}
@@ -213,14 +232,29 @@ public final class FileImportHelper {
}
// Build a collection of `ImportedFile`
- let processedFiles: [ImportedFile] = results.compactMap { taskResult in
+ let processedFiles: [ImportedFile] = try await results.concurrentCompactMap { taskResult in
guard case .success(let url) = taskResult else {
return nil
}
let fileName = url.lastPathComponent
let uti = UTI(filenameExtension: url.pathExtension) ?? UTI.data
- let importedFile = ImportedFile(name: fileName, path: url, uti: uti)
+
+ var finalUrl: URL
+ if self.appContextService.isExtension {
+ // In extension, we need to copy files to a path within appGroup to be able to upload from the main app.
+ let appGroupURL = try URL.appGroupUniqueFolderURL()
+
+ // Get import URL
+ let appGroupFileURL = appGroupURL.appendingPathComponent(fileName)
+ try self.fileManager.copyItem(atPath: url.path, toPath: appGroupFileURL.path)
+ finalUrl = appGroupFileURL
+ } else {
+ // Path obtained within the main app are stable, and will stay accessible.
+ finalUrl = url
+ }
+
+ let importedFile = ImportedFile(name: fileName, path: finalUrl, uti: uti)
return importedFile
}
@@ -237,3 +271,16 @@ public final class FileImportHelper {
return progress
}
}
+
+// TODO: move to core
+extension URL {
+ /// Build a path where a file can be moved within the appGroup while preventing collisions
+ static func appGroupUniqueFolderURL() throws -> URL {
+ // Use a unique folder to prevent collisions
+ @InjectService var pathProvider: AppGroupPathProvidable
+ let targetFolderURL = pathProvider.groupDirectoryURL
+ .appendingPathComponent(UUID().uuidString, isDirectory: true)
+ try FileManager.default.createDirectory(at: targetFolderURL, withIntermediateDirectories: true)
+ return targetFolderURL
+ }
+}
diff --git a/kDriveCore/Utils/KeychainHelper.swift b/kDriveCore/Utils/KeychainHelper.swift
index 179bbe164..7c90fe14f 100644
--- a/kDriveCore/Utils/KeychainHelper.swift
+++ b/kDriveCore/Utils/KeychainHelper.swift
@@ -111,19 +111,32 @@ public enum KeychainHelper {
if let savedToken = getSavedToken(for: token.userId) {
keychainQueue.sync {
+ let queryUpdate: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrAccount as String: "\(token.userId)"
+ ]
+
+ let attributes: [String: Any] = [
+ kSecValueData as String: tokenData
+ ]
+
// Save token only if it's more recent
- if savedToken.expirationDate <= token.expirationDate {
- let queryUpdate: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrAccount as String: "\(token.userId)"
- ]
-
- let attributes: [String: Any] = [
- kSecValueData as String: tokenData
- ]
+ if let savedTokenExpirationDate = savedToken.expirationDate,
+ let newTokenExpirationDate = token.expirationDate,
+ savedTokenExpirationDate <= newTokenExpirationDate {
resultCode = SecItemUpdate(queryUpdate as CFDictionary, attributes as CFDictionary)
DDLogInfo("Successfully updated token ? \(resultCode == noErr)")
-
+ let metadata = token.breadcrumbMetadata(keychainError: resultCode)
+ SentryDebug.addBreadcrumb(
+ message: "Successfully updated token",
+ category: .apiToken,
+ level: .info,
+ metadata: metadata
+ )
+ } else if savedToken.expirationDate == nil || token.expirationDate == nil {
+ // Or if one of them is now an infinite refresh token
+ resultCode = SecItemUpdate(queryUpdate as CFDictionary, attributes as CFDictionary)
+ DDLogInfo("Successfully updated unlimited token ? \(resultCode == noErr)")
let metadata = token.breadcrumbMetadata(keychainError: resultCode)
SentryDebug.addBreadcrumb(
message: "Successfully updated token",
diff --git a/kDriveCore/Utils/Logging.swift b/kDriveCore/Utils/Logging.swift
index 27fc422dc..78a1f0074 100644
--- a/kDriveCore/Utils/Logging.swift
+++ b/kDriveCore/Utils/Logging.swift
@@ -80,7 +80,8 @@ public enum Logging {
private static func initNetworkLogging() {
#if DEBUG && !TEST
- if !Bundle.main.isExtension {
+ @InjectService var appContextService: AppContextServiceable
+ if !appContextService.isExtension {
Atlantis.start(hostName: ProcessInfo.processInfo.environment["hostname"])
}
#endif
@@ -106,7 +107,11 @@ public enum Logging {
private static func copyDebugInformations() {
#if DEBUG && !TEST
- guard !Bundle.main.isExtension else { return }
+ @InjectService var appContextService: AppContextServiceable
+ guard !appContextService.isExtension else {
+ return
+ }
+
let fileManager = FileManager.default
let debugDirectory = (fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(
"debug",
diff --git a/kDriveCore/Utils/NotificationsHelper.swift b/kDriveCore/Utils/NotificationsHelper.swift
index c1885639b..b161083d4 100644
--- a/kDriveCore/Utils/NotificationsHelper.swift
+++ b/kDriveCore/Utils/NotificationsHelper.swift
@@ -20,6 +20,7 @@ import CocoaLumberjackSwift
import Foundation
import InfomaniakCore
import InfomaniakCoreUI
+import InfomaniakDI
import kDriveResources
import UserNotifications
@@ -45,6 +46,8 @@ public protocol NotificationsHelpable {
}
public struct NotificationsHelper: NotificationsHelpable {
+ @LazyInjectService private var appContextService: AppContextServiceable
+
public enum CategoryIdentifier {
public static let general = "com.kdrive.notification.general"
public static let upload = "com.kdrive.notification.upload"
@@ -218,19 +221,14 @@ public struct NotificationsHelper: NotificationsHelpable {
return
}
- let isInBackground = Bundle.main.isExtension || UIApplication.shared.applicationState != .active
+ let isInBackground = appContextService.isExtension || UIApplication.shared.applicationState != .active
if isInBackground {
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: id, content: notification, trigger: trigger)
UNUserNotificationCenter.current().add(request)
} else {
- let snackbar = IKSnackBar.make(message: notification.body, duration: .lengthLong)
- if let action {
- snackbar?.setAction(action).show()
- } else {
- snackbar?.show()
- }
+ UIConstants.showSnackBar(message: notification.body, duration: .lengthLong, action: action)
}
}
}
diff --git a/kDriveCore/Utils/PHAsset/PHAsset+Exension.swift b/kDriveCore/Utils/PHAsset/PHAsset+Exension.swift
index dec06d7ba..4d38843a2 100644
--- a/kDriveCore/Utils/PHAsset/PHAsset+Exension.swift
+++ b/kDriveCore/Utils/PHAsset/PHAsset+Exension.swift
@@ -28,20 +28,26 @@ public extension PHAsset {
///
/// Will return `nil` for any other resource type (like video)
var baseImageSHA256: String? {
- guard let identifier = PHAssetIdentifier(self) else {
- return nil
- }
+ get throws {
+ guard let identifier = PHAssetIdentifier(self) else {
+ return nil
+ }
- return identifier.baseImageSHA256
+ let hash = try identifier.baseImageSHA256
+ return hash
+ }
}
/// Hash of the best resource available. Editing a video or a picture will change this hash
var bestResourceSHA256: String? {
- guard let identifier = PHAssetIdentifier(self) else {
- return nil
- }
+ get throws {
+ guard let identifier = PHAssetIdentifier(self) else {
+ return nil
+ }
- return identifier.bestResourceSHA256
+ let hash = try identifier.bestResourceSHA256
+ return hash
+ }
}
// MARK: - Filename
diff --git a/kDriveCore/Utils/PHAsset/PHAssetIdentifier.swift b/kDriveCore/Utils/PHAsset/PHAssetIdentifier.swift
index 6150585f5..1c460eacd 100644
--- a/kDriveCore/Utils/PHAsset/PHAssetIdentifier.swift
+++ b/kDriveCore/Utils/PHAsset/PHAssetIdentifier.swift
@@ -32,13 +32,36 @@ protocol PHAssetIdentifiable {
/// Get a hash of the base image of a PHAsset _without adjustments_
///
/// Other types of PHAsset will return nil
- var baseImageSHA256: String? { get }
+ /// - Throws: if the system is trying to end a background activity
+ var baseImageSHA256: String? { get throws }
/// Computes the SHA of the `.bestResource` available
///
/// Anything accessible with a `requestData` will work (photo / video …)
/// A picture with adjustments will see its hash change here
- var bestResourceSHA256: String? { get }
+ /// - Throws: if the system is trying to end a background activity
+ var bestResourceSHA256: String? { get throws }
+}
+
+/// Something that will be called in case an asset activity is expiring
+///
+/// It will unlock a linked TolerantDispatchGroup, and write an error to a given reference
+final class AssetExpiringActivityDelegate: ExpiringActivityDelegate {
+ enum ErrorDomain: Error {
+ case assetActivityExpired
+ }
+
+ let group: TolerantDispatchGroup
+ var error: Error?
+
+ init(group: TolerantDispatchGroup) {
+ self.group = group
+ }
+
+ func backgroundActivityExpiring() {
+ error = ErrorDomain.assetActivityExpired
+ group.leave()
+ }
}
struct PHAssetIdentifier: PHAssetIdentifiable {
@@ -53,75 +76,106 @@ struct PHAssetIdentifier: PHAssetIdentifiable {
}
var baseImageSHA256: String? {
- guard #available(iOS 15, *) else {
- return nil
- }
+ get throws {
+ guard #available(iOS 15, *) else {
+ return nil
+ }
- let group = TolerantDispatchGroup()
- var hash: String?
+ // We build an ExpiringActivity to track system termination
+ let uid = "\(#function)-\(asset.localIdentifier)-\(UUID().uuidString)"
+ let group = TolerantDispatchGroup()
+ let activityDelegate = AssetExpiringActivityDelegate(group: group)
+ let activity = ExpiringActivity(id: uid, delegate: activityDelegate)
+ activity.start()
- let options = PHContentEditingInputRequestOptions()
- options.canHandleAdjustmentData = { _ -> Bool in
- return true
- }
+ var hash: String?
- // Trigger a request in order to intercept change data
- asset.requestContentEditingInput(with: options) { input, _ in
- defer {
- group.leave()
+ let options = PHContentEditingInputRequestOptions()
+ options.canHandleAdjustmentData = { _ -> Bool in
+ return true
}
- guard let input else {
- return
- }
+ // Trigger a request in order to intercept change data
+ asset.requestContentEditingInput(with: options) { input, _ in
+ defer {
+ group.leave()
+ }
+
+ guard let input else {
+ return
+ }
- guard let url = input.fullSizeImageURL else {
- return
+ guard let url = input.fullSizeImageURL else {
+ return
+ }
+
+ // Hashing the raw data of the picture is the only reliable solution to know when effects were applied
+ // This will exclude changes related to like and albums
+ hash = url.dataRepresentation.SHA256DigestString
}
- // Hashing the raw data of the picture is the only reliable solution to know when effects were applied
- // This will exclude changes related to like and albums
- hash = url.dataRepresentation.SHA256DigestString
- }
+ // wait for the request to finish
+ group.enter()
+ group.wait()
+ activity.endAll()
- // wait for the request to finish
- group.enter()
- group.wait()
+ guard let error = activityDelegate.error else {
+ // All good
+ return hash
+ }
- return hash
+ // The processing of the hash was interrupted by the system
+ throw error
+ }
}
var bestResourceSHA256: String? {
- guard #available(iOS 15, *) else {
- return nil
- }
+ get throws {
+ guard #available(iOS 15, *) else {
+ return nil
+ }
- guard let bestResource = asset.bestResource else {
- return baseImageSHA256
- }
+ guard let bestResource = asset.bestResource else {
+ let hashFallback = try baseImageSHA256
+ return hashFallback
+ }
- let group = TolerantDispatchGroup()
- let hasher = StreamHasher()
+ let hasher = StreamHasher()
- // TODO: Check iCloud behaviour
- let options = PHAssetResourceRequestOptions()
- options.isNetworkAccessAllowed = true
- options.progressHandler = { progress in
- Log.photoLibraryUploader("hashing resource \(progress * 100)% …")
- }
+ // We build an ExpiringActivity to track system termination
+ let uid = "\(#function)-\(asset.localIdentifier)-\(UUID().uuidString)"
+ let group = TolerantDispatchGroup()
+ let activityDelegate = AssetExpiringActivityDelegate(group: group)
+ let activity = ExpiringActivity(id: uid, delegate: activityDelegate)
+ activity.start()
- PHAssetResourceManager.default().requestData(for: bestResource,
- options: options) { data in
- hasher.update(data)
- } completionHandler: { error in
- hasher.finalize()
- group.leave()
- }
+ // TODO: Check iCloud behaviour
+ let options = PHAssetResourceRequestOptions()
+ options.isNetworkAccessAllowed = true
+ options.progressHandler = { progress in
+ Log.photoLibraryUploader("hashing resource \(progress * 100)% …")
+ }
+
+ PHAssetResourceManager.default().requestData(for: bestResource,
+ options: options) { data in
+ hasher.update(data)
+ } completionHandler: { error in
+ hasher.finalize()
+ group.leave()
+ }
+
+ // wait for the request to finish
+ group.enter()
+ group.wait()
+ activity.endAll()
- // wait for the request to finish
- group.enter()
- group.wait()
+ guard let error = activityDelegate.error else {
+ // All good
+ return hasher.digestString
+ }
- return hasher.digestString
+ // The processing of the hash was interrupted by the system
+ throw error
+ }
}
}
diff --git a/kDriveCore/Utils/Sentry/ApiToken+Sentry.swift b/kDriveCore/Utils/Sentry/ApiToken+Sentry.swift
index 390b9323d..f712bf748 100644
--- a/kDriveCore/Utils/Sentry/ApiToken+Sentry.swift
+++ b/kDriveCore/Utils/Sentry/ApiToken+Sentry.swift
@@ -18,11 +18,12 @@
import Foundation
import InfomaniakCore
+import InfomaniakLogin
public extension ApiToken {
func breadcrumbMetadata(keychainError: OSStatus = noErr) -> [String: Any] {
return ["User id": userId,
- "Expiration date": expirationDate.timeIntervalSince1970,
+ "Expiration date": expirationDate?.timeIntervalSince1970 ?? "",
"Access Token": truncatedAccessToken,
"Refresh Token": truncatedRefreshToken,
"Keychain error code": keychainError]
diff --git a/kDriveCore/Utils/Sentry/SentryDebug.swift b/kDriveCore/Utils/Sentry/SentryDebug.swift
index 4bf224589..20b4bc3e4 100644
--- a/kDriveCore/Utils/Sentry/SentryDebug.swift
+++ b/kDriveCore/Utils/Sentry/SentryDebug.swift
@@ -17,6 +17,7 @@
*/
import Foundation
+import InfomaniakLogin
import Sentry
import UIKit
@@ -50,6 +51,25 @@ public enum SentryDebug {
static let viewModelNotConnectedToView = "ViewModelNotConnected"
}
+ static func logTokenMigration(newToken: ApiToken, oldToken: ApiToken) {
+ let newTokenIsInfinite = newToken.expirationDate == nil
+ let oldTokenIsInfinite = oldToken.expirationDate == nil
+
+ let additionalData = ["newTokenIsInfinite": newTokenIsInfinite, "oldTokenIsInfinite": oldTokenIsInfinite]
+ let breadcrumb = Breadcrumb(level: .info, category: "Token")
+ breadcrumb.message = "Token updated"
+ breadcrumb.data = additionalData
+ SentrySDK.addBreadcrumb(breadcrumb)
+
+ // Only track migration
+ guard newTokenIsInfinite else { return }
+
+ SentrySDK.capture(message: "Update token infinite token") { scope in
+ scope.setContext(value: additionalData,
+ key: "Migration context")
+ }
+ }
+
// MARK: - View Model Observation
public static func viewModelObservationError(_ function: String = #function) {
diff --git a/kDriveFileProvider/FileProviderExtension.swift b/kDriveFileProvider/FileProviderExtension.swift
index 66e6ae2ef..145d17f86 100644
--- a/kDriveFileProvider/FileProviderExtension.swift
+++ b/kDriveFileProvider/FileProviderExtension.swift
@@ -26,7 +26,7 @@ import RealmSwift
final class FileProviderExtension: NSFileProviderExtension {
/// Making sure the DI is registered at a very early stage of the app launch.
- private let dependencyInjectionHook = EarlyDIHook()
+ private let dependencyInjectionHook = EarlyDIHook(context: .fileProviderExtension)
/// Something to enqueue async await tasks in a serial manner.
let asyncAwaitQueue = TaskQueue()
@@ -337,7 +337,7 @@ final class FileProviderExtension: NSFileProviderExtension {
self.fileProviderState.removeWorkingDocument(forKey: item.itemIdentifier)
}
- _ = self.uploadQueue.saveToRealmAndAddToQueue(uploadFile: uploadFile, itemIdentifier: item.itemIdentifier)
+ _ = self.uploadQueue.saveToRealm(uploadFile, itemIdentifier: item.itemIdentifier, addToQueue: true)
}
}
diff --git a/kDriveShareExtension/ShareNavigationViewController.swift b/kDriveShareExtension/ShareNavigationViewController.swift
index ee5b705bc..2c955d31f 100644
--- a/kDriveShareExtension/ShareNavigationViewController.swift
+++ b/kDriveShareExtension/ShareNavigationViewController.swift
@@ -21,10 +21,11 @@ import InfomaniakDI
import InfomaniakLogin
import kDriveCore
import UIKit
+import VersionChecker
final class ShareNavigationViewController: TitleSizeAdjustingNavigationController {
/// Making sure the DI is registered at a very early stage of the app launch.
- private let dependencyInjectionHook = EarlyDIHook()
+ private let dependencyInjectionHook = EarlyDIHook(context: .shareExtension)
@LazyInjectService var accountManager: AccountManageable
@@ -44,6 +45,10 @@ final class ShareNavigationViewController: TitleSizeAdjustingNavigationControlle
saveViewController.itemProviders = attachments
viewControllers = [saveViewController]
+
+ Task {
+ try? await checkAppVersion()
+ }
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
@@ -52,4 +57,15 @@ final class ShareNavigationViewController: TitleSizeAdjustingNavigationControlle
}
extensionContext.completeRequest(returningItems: nil, completionHandler: nil)
}
+
+ private func checkAppVersion() async throws {
+ guard try await VersionChecker.standard.checkAppVersionStatus() == .updateIsRequired else { return }
+ Task { @MainActor in
+ let updateRequiredViewController = DriveUpdateRequiredViewController()
+ updateRequiredViewController.dismissHandler = { [weak self] in
+ self?.dismiss(animated: true)
+ }
+ viewControllers = [updateRequiredViewController]
+ }
+ }
}
diff --git a/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift b/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift
index 8b0fbc2ed..f9cbab131 100644
--- a/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift
+++ b/kDriveTests/kDrive/Launch/ITAppLaunchTest.swift
@@ -26,6 +26,8 @@ import RealmSwift
import XCTest
final class ITAppLaunchTest: XCTestCase {
+ let loginConfig = InfomaniakLogin.Config(clientId: "9473D73C-C20F-4971-9E10-D957C563FA68", accessType: nil)
+
let fakeAccount = Account(apiToken: ApiToken(
accessToken: "",
expiresIn: 0,
@@ -42,7 +44,7 @@ final class ITAppLaunchTest: XCTestCase {
SimpleResolver.register(FactoryService.debugServices)
let services = [
Factory(type: InfomaniakNetworkLogin.self) { _, _ in
- return InfomaniakNetworkLogin(clientId: "", redirectUri: "")
+ return InfomaniakNetworkLogin(config: self.loginConfig)
},
Factory(type: UploadQueue.self) { _, _ in
UploadQueue()
@@ -66,7 +68,7 @@ final class ITAppLaunchTest: XCTestCase {
resolver: resolver)
},
Factory(type: InfomaniakLoginable.self) { _, _ in
- InfomaniakLogin(clientId: DriveApiFetcher.clientId)
+ InfomaniakLogin(config: self.loginConfig)
},
Factory(type: AppLockHelper.self) { _, _ in
AppLockHelper()
diff --git a/kDriveTests/kDrive/Launch/MockAccountManager.swift b/kDriveTests/kDrive/Launch/MockAccountManager.swift
index 22fbe88e7..bda697704 100644
--- a/kDriveTests/kDrive/Launch/MockAccountManager.swift
+++ b/kDriveTests/kDrive/Launch/MockAccountManager.swift
@@ -18,6 +18,7 @@
import Foundation
import InfomaniakCore
+import InfomaniakLogin
import kDriveCore
import RealmSwift
diff --git a/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift b/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift
index b61c8df31..e915f0080 100644
--- a/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift
+++ b/kDriveTests/kDrive/Launch/UTRootViewControllerState.swift
@@ -26,6 +26,8 @@ import RealmSwift
import XCTest
final class UTRootViewControllerState: XCTestCase {
+ let loginConfig = InfomaniakLogin.Config(clientId: "9473D73C-C20F-4971-9E10-D957C563FA68", accessType: nil)
+
let fakeAccount = Account(apiToken: ApiToken(
accessToken: "",
expiresIn: 0,
@@ -41,7 +43,7 @@ final class UTRootViewControllerState: XCTestCase {
let services = [
Factory(type: InfomaniakNetworkLogin.self) { _, _ in
- InfomaniakNetworkLogin(clientId: "", redirectUri: "")
+ InfomaniakNetworkLogin(config: self.loginConfig)
},
Factory(type: UploadQueue.self) { _, _ in
UploadQueue()
@@ -65,7 +67,7 @@ final class UTRootViewControllerState: XCTestCase {
resolver: resolver)
},
Factory(type: InfomaniakLoginable.self) { _, _ in
- InfomaniakLogin(clientId: DriveApiFetcher.clientId)
+ InfomaniakLogin(config: self.loginConfig)
},
Factory(type: AppLockHelper.self) { _, _ in
AppLockHelper()
diff --git a/kDriveTests/kDriveCore/MenuViewControllerTests.swift b/kDriveTests/kDriveCore/MenuViewControllerTests.swift
index 30330fd6b..24f440358 100644
--- a/kDriveTests/kDriveCore/MenuViewControllerTests.swift
+++ b/kDriveTests/kDriveCore/MenuViewControllerTests.swift
@@ -19,6 +19,7 @@
@testable import Alamofire
@testable import InfomaniakCore
@testable import InfomaniakDI
+import InfomaniakLogin
@testable import kDrive
@testable import kDriveCore
import XCTest