diff --git a/.package.resolved b/.package.resolved index 9f8ace9bd..70590637a 100644 --- a/.package.resolved +++ b/.package.resolved @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SCENEE/FloatingPanel", "state" : { - "revision" : "8f2be39bf49b4d5e22bbf7bdde69d5b76d0ecd2a", - "version" : "2.8.2" + "revision" : "22d46c526084724a718b8c39ab77f12452712cc7", + "version" : "2.8.3" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/raspu/Highlightr", "state" : { - "revision" : "93199b9e434f04bda956a613af8f571933f9f037", - "version" : "2.1.2" + "revision" : "fa483d37c692961ecc2391eac568ba3d3935e663", + "version" : "2.2.0" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-core", "state" : { - "revision" : "6ec34998530da3e5ebf92c1a1b0209cd65cff3b6", - "version" : "10.0.0" + "revision" : "33ce7bc356d9938078a4ec596a535390e25d8650", + "version" : "10.0.3" } }, { @@ -132,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher", "state" : { - "revision" : "5b92f029fab2cce44386d28588098b5be0824ef5", - "version" : "7.11.0" + "revision" : "2ef543ee21d63734e1c004ad6c870255e8716c50", + "version" : "7.12.0" } }, { @@ -213,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core.git", "state" : { - "revision" : "374dd672af357732dccc135fecc905406fec3223", - "version" : "14.4.1" + "revision" : "e55176982ed9154899a39693d24609645b81586b", + "version" : "14.3.0" } }, { @@ -222,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-swift", "state" : { - "revision" : "e0c2fbb442979fbf1e4be80e01d142f310a9c762", - "version" : "10.49.1" + "revision" : "0a97dda4dd0b77449e55b997cf636651e6187634", + "version" : "10.49.0" } }, { @@ -231,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "ef4fec9dfb8dd5027b09a4a5c9362feafd118e1a", - "version" : "8.24.0" + "revision" : "8fd4e804f2e72e0b9c1b189ce4e8349c4d10b6a2", + "version" : "8.30.0" } }, { @@ -276,8 +276,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", + "version" : "1.1.1" } }, { @@ -285,8 +285,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/swift-concurrency", "state" : { - "revision" : "02960fd5d2cf57c7ba38d13bbbf580d7f6ac7102", - "version" : "0.0.5" + "revision" : "55fe7541cbfcfb4b73e7836b390e2c566a9ba910", + "version" : "0.0.6" } }, { @@ -294,8 +294,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log", "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" } }, { @@ -303,8 +303,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", - "version" : "2.64.0" + "revision" : "e5a216ba89deba84356bad9d4c2eab99071c745b", + "version" : "2.67.0" } }, { @@ -312,8 +312,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", - "version" : "2.26.0" + "revision" : "2b09805797f21c380f7dc9bedaab3157c5508efb", + "version" : "2.27.0" } }, { @@ -321,8 +321,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", - "version" : "1.20.1" + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" } }, { @@ -339,8 +339,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", - "version" : "1.2.1" + "revision" : "6a9e38e7bd22a3b8ba80bddf395623cf68f57807", + "version" : "1.3.1" } }, { diff --git a/Project.swift b/Project.swift index a5c87fd70..451b094a3 100644 --- a/Project.swift +++ b/Project.swift @@ -30,7 +30,7 @@ let project = Project(name: "kDrive", .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/Infomaniak/ios-version-checker", .upToNextMajor(from: "5.0.0")), - .package(url: "https://github.com/realm/realm-swift", .upToNextMajor(from: "10.43.0")), + .package(url: "https://github.com/realm/realm-swift", .exact("10.49.0")), .package(url: "https://github.com/SCENEE/FloatingPanel", .upToNextMajor(from: "2.0.0")), .package(url: "https://github.com/onevcat/Kingfisher", .upToNextMajor(from: "7.6.2")), .package(url: "https://github.com/flowbe/MaterialOutlinedTextField", .upToNextMajor(from: "0.1.0")), @@ -93,8 +93,8 @@ let project = Project(name: "kDrive", deploymentTarget: Constants.deploymentTarget, infoPlist: .default, sources: [ - "kDriveTests/**", - "kDriveTestShared/**" + "kDriveTests/**", + "kDriveTestShared/**" ], resources: [ "kDriveTests/**/*.jpg", @@ -111,8 +111,8 @@ let project = Project(name: "kDrive", deploymentTarget: Constants.deploymentTarget, infoPlist: .default, sources: [ - "kDriveAPITests/**", - "kDriveTestShared/**" + "kDriveAPITests/**", + "kDriveTestShared/**" ], dependencies: [ .target(name: "kDrive") @@ -181,11 +181,12 @@ let project = Project(name: "kDrive", deploymentTarget: Constants.deploymentTarget, infoPlist: .file(path: "kDriveFileProvider/Info.plist"), sources: [ - "kDriveFileProvider/**", - "kDrive/Utils/AppFactoryService.swift", - "kDrive/Utils/NavigationManager.swift"], + "kDriveFileProvider/**", + "kDrive/Utils/AppFactoryService.swift", + "kDrive/Utils/NavigationManager.swift" + ], resources: [ - "kDrive/**/PrivacyInfo.xcprivacy" + "kDrive/**/PrivacyInfo.xcprivacy" ], headers: .headers(project: "kDriveFileProvider/**"), entitlements: "kDriveFileProvider/FileProvider.entitlements", diff --git a/Tuist/ProjectDescriptionHelpers/Constants.swift b/Tuist/ProjectDescriptionHelpers/Constants.swift index 46d49df85..cd56ea72f 100644 --- a/Tuist/ProjectDescriptionHelpers/Constants.swift +++ b/Tuist/ProjectDescriptionHelpers/Constants.swift @@ -20,7 +20,7 @@ import ProjectDescription public enum Constants { public static let testSettings: [String: SettingValue] = [ - "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "TEST DEBUG" + "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "DEBUG" ] public static let baseSettings = SettingsDictionary() diff --git a/kDrive/AppDelegate+Launch.swift b/kDrive/AppDelegate+Launch.swift deleted file mode 100644 index 121a178e6..000000000 --- a/kDrive/AppDelegate+Launch.swift +++ /dev/null @@ -1,287 +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 Foundation -import InfomaniakCore -import InfomaniakDI -import kDriveCore -import kDriveResources -import SafariServices -import StoreKit -import UIKit - -extension AppDelegate { - // MARK: Launch - - func prepareRootViewController(currentState: RootViewControllerState) { - switch currentState { - case .appLock: - showAppLock() - case .mainViewController(let driveFileManager): - showMainViewController(driveFileManager: driveFileManager) - showLaunchFloatingPanel() - askForReview() - askUserToRemovePicturesIfNecessary() - case .onboarding: - showOnboarding() - case .updateRequired: - showUpdateRequired() - case .preloading(let currentAccount): - showPreloading(currentAccount: currentAccount) - } - } - - func updateRootViewControllerState() { - let newState = RootViewControllerState.getCurrentState() - prepareRootViewController(currentState: newState) - } - - // MARK: Set root VC - - func showMainViewController(driveFileManager: DriveFileManager) { - guard let window else { - SentryDebug.captureNoWindow() - return - } - - let currentDriveObjectId = (window.rootViewController as? MainTabViewController)?.driveFileManager.drive.objectId - guard currentDriveObjectId != driveFileManager.drive.objectId else { - return - } - - window.rootViewController = MainTabViewController(driveFileManager: driveFileManager) - window.makeKeyAndVisible() - } - - func showPreloading(currentAccount: Account) { - guard let window else { - SentryDebug.captureNoWindow() - return - } - - window.rootViewController = PreloadingViewController(currentAccount: currentAccount) - window.makeKeyAndVisible() - } - - private func showOnboarding() { - guard let window else { - SentryDebug.captureNoWindow() - return - } - - defer { - // Clean File Provider domains on first launch in case we had some dangling - driveInfosManager.deleteAllFileProviderDomains() - } - - // Check if presenting onboarding - let isNotPresentingOnboarding = window.rootViewController?.isKind(of: OnboardingViewController.self) != true - guard isNotPresentingOnboarding else { - return - } - - keychainHelper.deleteAllTokens() - window.rootViewController = OnboardingViewController.instantiate() - window.makeKeyAndVisible() - } - - private func showAppLock() { - guard let window else { - SentryDebug.captureNoWindow() - return - } - - window.rootViewController = LockedAppViewController.instantiate() - window.makeKeyAndVisible() - } - - private func showLaunchFloatingPanel() { - guard let window else { - SentryDebug.captureNoWindow() - return - } - - let launchPanelsController = LaunchPanelsController() - if let viewController = window.rootViewController { - launchPanelsController.pickAndDisplayPanel(viewController: viewController) - } - } - - private func showUpdateRequired() { - guard let window else { - SentryDebug.captureNoWindow() - return - } - - window.rootViewController = DriveUpdateRequiredViewController() - window.makeKeyAndVisible() - } - - // MARK: Misc - - private func askForReview() { - guard let presentingViewController = window?.rootViewController, - !Bundle.main.isRunningInTestFlight - else { return } - - let shouldRequestReview = reviewManager.shouldRequestReview() - - if shouldRequestReview { - let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String - let alert = AlertTextViewController( - title: appName, - message: KDriveResourcesStrings.Localizable.reviewAlertTitle, - action: KDriveResourcesStrings.Localizable.buttonYes, - hasCancelButton: true, - cancelString: KDriveResourcesStrings.Localizable.buttonNo, - handler: requestAppStoreReview, - cancelHandler: openUserReport - ) - - presentingViewController.present(alert, animated: true) - MatomoUtils.track(eventWithCategory: .appReview, name: "alertPresented") - } - } - - private func requestAppStoreReview() { - MatomoUtils.track(eventWithCategory: .appReview, name: "like") - UserDefaults.shared.appReview = .readyForReview - reviewManager.requestReview() - } - - private func openUserReport() { - MatomoUtils.track(eventWithCategory: .appReview, name: "dislike") - guard let url = URL(string: KDriveResourcesStrings.Localizable.urlUserReportiOS), - let presentingViewController = window?.rootViewController else { - return - } - UserDefaults.shared.appReview = .feedback - presentingViewController.present(SFSafariViewController(url: url), animated: true) - } - - // TODO: Refactor to async - func uploadEditedFiles() { - Log.appDelegate("uploadEditedFiles") - guard let folderURL = DriveFileManager.constants.openInPlaceDirectoryURL, - FileManager.default.fileExists(atPath: folderURL.path) else { - return - } - - let group = DispatchGroup() - var shouldCleanFolder = false - let driveFolders = (try? FileManager.default.contentsOfDirectory(atPath: folderURL.path)) ?? [] - // Hierarchy inside folderURL should be /driveId/fileId/fileName.extension - for driveFolder in driveFolders { - // Read drive folder - let driveFolderURL = folderURL.appendingPathComponent(driveFolder) - guard let driveId = Int(driveFolder), - let drive = driveInfosManager.getDrive(id: driveId, userId: accountManager.currentUserId), - let fileFolders = try? FileManager.default.contentsOfDirectory(atPath: driveFolderURL.path) else { - Log.appDelegate("[OPEN-IN-PLACE UPLOAD] Could not infer drive from \(driveFolderURL)") - continue - } - - for fileFolder in fileFolders { - // Read file folder - let fileFolderURL = driveFolderURL.appendingPathComponent(fileFolder) - guard let fileId = Int(fileFolder), - let driveFileManager = accountManager.getDriveFileManager(for: drive), - let file = driveFileManager.getCachedFile(id: fileId) else { - Log.appDelegate("[OPEN-IN-PLACE UPLOAD] Could not infer file from \(fileFolderURL)") - continue - } - - let fileURL = fileFolderURL.appendingPathComponent(file.name) - guard FileManager.default.fileExists(atPath: fileURL.path) else { - continue - } - - // Compare modification date - let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path) - let modificationDate = attributes?[.modificationDate] as? Date ?? Date(timeIntervalSince1970: 0) - - guard modificationDate > file.lastModifiedAt else { - continue - } - - // Copy and upload file - let uploadFile = UploadFile(parentDirectoryId: file.parentId, - userId: accountManager.currentUserId, - driveId: file.driveId, - url: fileURL, - name: file.name, - conflictOption: .version, - shouldRemoveAfterUpload: false) - group.enter() - shouldCleanFolder = true - @InjectService var uploadQueue: UploadQueue - var observationToken: ObservationToken? - observationToken = uploadQueue - .observeFileUploaded(self, fileId: uploadFile.id) { [fileId = file.id] uploadFile, _ in - observationToken?.cancel() - if let error = uploadFile.error { - shouldCleanFolder = false - Log.appDelegate("[OPEN-IN-PLACE UPLOAD] Error while uploading: \(error)", level: .error) - } else { - // Update file to get the new modification date - Task { - let file = try await driveFileManager.file(id: fileId, forceRefresh: true) - try? FileManager.default.setAttributes([.modificationDate: file.lastModifiedAt], - ofItemAtPath: file.localUrl.path) - driveFileManager.notifyObserversWith(file: file) - } - } - group.leave() - } - uploadQueue.saveToRealm(uploadFile, itemIdentifier: nil) - } - } - - // Clean folder after completing all uploads - group.notify(queue: DispatchQueue.global(qos: .utility)) { - if shouldCleanFolder { - Log.appDelegate("[OPEN-IN-PLACE UPLOAD] Cleaning folder") - try? FileManager.default.removeItem(at: folderURL) - } - } - } - - /// Ask the user to remove pictures if configured - private func askUserToRemovePicturesIfNecessary() { - @InjectService var photoCleaner: PhotoLibraryCleanerServiceable - guard photoCleaner.hasPicturesToRemove else { - Log.appDelegate("No pictures to remove", level: .info) - return - } - - let alert = AlertTextViewController(title: KDriveResourcesStrings.Localizable.modalDeletePhotosTitle, - message: KDriveResourcesStrings.Localizable.modalDeletePhotosDescription, - action: KDriveResourcesStrings.Localizable.buttonDelete, - destructive: true, - loading: false) { - Task { - // Proceed with removal - await photoCleaner.removePicturesScheduledForDeletion() - } - } - - Task { @MainActor in - self.window?.rootViewController?.present(alert, animated: true) - } - } -} diff --git a/kDrive/AppDelegate+Scene.swift b/kDrive/AppDelegate+Scene.swift new file mode 100644 index 000000000..1c30c1ced --- /dev/null +++ b/kDrive/AppDelegate+Scene.swift @@ -0,0 +1,33 @@ +/* + 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 kDriveCore +import UIKit + +extension AppDelegate { + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + Log.appDelegate("application configurationForConnecting:\(connectingSceneSession)") + return connectingSceneSession.configuration + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + Log.appDelegate("application didDiscardSceneSessions:\(sceneSessions)") + } +} diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index b9ada886f..64da65562 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -34,30 +34,18 @@ import UserNotifications import VersionChecker @main -final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDelegate { +final class AppDelegate: UIResponder, UIApplicationDelegate { /// Making sure the DI is registered at a very early stage of the app launch. private let dependencyInjectionHook = EarlyDIHook(context: .app) private var reachabilityListener: ReachabilityListener! - private var shortcutItemToProcess: UIApplicationShortcutItem? - var window: UIWindow? - - @LazyInjectService var lockHelper: AppLockHelper @LazyInjectService var infomaniakLogin: InfomaniakLogin - @LazyInjectService var backgroundUploadSessionManager: BackgroundUploadSessionManager - @LazyInjectService var backgroundDownloadSessionManager: BackgroundDownloadSessionManager - @LazyInjectService var photoLibraryUploader: PhotoLibraryUploader @LazyInjectService var notificationHelper: NotificationsHelpable - @LazyInjectService var driveInfosManager: DriveInfosManager - @LazyInjectService var keychainHelper: KeychainHelper + @LazyInjectService var accountManager: AccountManageable @LazyInjectService var backgroundTasksService: BackgroundTasksServiceable - @LazyInjectService var reviewManager: ReviewManageable - @LazyInjectService var availableOfflineManager: AvailableOfflineManageable - @LazyInjectService var appRestorationService: AppRestorationService - - // Not lazy to force init of the object early, and set a userID in Sentry - @InjectService var accountManager: AccountManageable + @LazyInjectService var appRestorationService: AppRestorationServiceable + @LazyInjectService private var appNavigable: AppNavigable // MARK: - UIApplicationDelegate @@ -85,33 +73,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg notificationHelper.registerCategories() UNUserNotificationCenter.current().delegate = self - window = UIWindow() - setGlobalTint() - - let state = UIApplication.shared.applicationState - if state != .background { - appWillBePresentedToTheUser() - } - - accountManager.delegate = self - if CommandLine.arguments.contains("testing") { UIView.setAnimationsEnabled(false) } - window?.overrideUserInterfaceStyle = UserDefaults.shared.theme.interfaceStyle - // Attach an observer to the payment queue. SKPaymentQueue.default().add(StoreObserver.shared) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleLocateUploadNotification), - name: .locateUploadActionTapped, - object: nil - ) - NotificationCenter.default.addObserver(self, selector: #selector(reloadDrive), name: .reloadDrive, object: nil) - return true } @@ -129,10 +97,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg } application.registerForRemoteNotifications() - if let shortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem { - shortcutItemToProcess = shortcutItem - } - return true } @@ -175,177 +139,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg Log.appDelegate("Unable to register for remote notifications: \(error.localizedDescription)", level: .error) } - func applicationDidEnterBackground(_ application: UIApplication) { - Log.appDelegate("applicationDidEnterBackground") - backgroundTasksService.scheduleBackgroundRefresh() - - if UserDefaults.shared.isAppLockEnabled, - !(window?.rootViewController?.isKind(of: LockedAppViewController.self) ?? false) { - lockHelper.setTime() - } - } - - func application( - _ application: UIApplication, - performActionFor shortcutItem: UIApplicationShortcutItem, - completionHandler: @escaping (Bool) -> Void - ) { - shortcutItemToProcess = shortcutItem - } - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { Log.appDelegate("application app open url\(url)") return DeeplinkParser().parse(url: url) } - func applicationWillEnterForeground(_ application: UIApplication) { - Log.appDelegate("applicationWillEnterForeground") - appWillBePresentedToTheUser() - } - - private func appWillBePresentedToTheUser() { - @InjectService var uploadQueue: UploadQueue - uploadQueue.pausedNotificationSent = false - - let currentState = RootViewControllerState.getCurrentState() - prepareRootViewController(currentState: currentState) - switch currentState { - case .mainViewController, .appLock: - UserDefaults.shared.numberOfConnections += 1 - UserDefaults.shared.openingUntilReview -= 1 - refreshCacheScanLibraryAndUpload(preload: false, isSwitching: false) - uploadEditedFiles() - case .onboarding, .updateRequired, .preloading: break - } - - // Remove all notifications on App Opening - UNUserNotificationCenter.current().removeAllDeliveredNotifications() - - Task { - if try await VersionChecker.standard.checkAppVersionStatus() == .updateIsRequired { - prepareRootViewController(currentState: .updateRequired) - } - } - } - - /// Set global tint color - private func setGlobalTint() { - window?.tintColor = KDriveResourcesAsset.infomaniakColor.color - UITabBar.appearance().unselectedItemTintColor = KDriveResourcesAsset.iconColor.color - // Migration from old UserDefaults - if UserDefaults.shared.legacyIsFirstLaunch { - UserDefaults.shared.legacyIsFirstLaunch = UserDefaults.standard.legacyIsFirstLaunch - } - } - - func applicationDidBecomeActive(_ application: UIApplication) { - if let shortcutItem = shortcutItemToProcess { - guard let rootViewController = window?.rootViewController as? MainTabViewController else { - return - } - - // Dismiss all view controllers presented - rootViewController.dismiss(animated: false) - - guard let navController = rootViewController.selectedViewController as? UINavigationController, - let viewController = navController.topViewController, - let driveFileManager = accountManager.currentDriveFileManager else { - return - } - - switch shortcutItem.type { - case Constants.applicationShortcutScan: - let openMediaHelper = OpenMediaHelper(driveFileManager: driveFileManager) - openMediaHelper.openScan(rootViewController, false) - MatomoUtils.track(eventWithCategory: .shortcuts, name: "scan") - case Constants.applicationShortcutSearch: - let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager) - viewController.present( - SearchViewController.instantiateInNavigationController(viewModel: viewModel), - animated: true - ) - MatomoUtils.track(eventWithCategory: .shortcuts, name: "search") - case Constants.applicationShortcutUpload: - let openMediaHelper = OpenMediaHelper(driveFileManager: driveFileManager) - openMediaHelper.openMedia(rootViewController, .library) - MatomoUtils.track(eventWithCategory: .shortcuts, name: "upload") - case Constants.applicationShortcutSupport: - UIApplication.shared.open(URLConstants.support.url) - MatomoUtils.track(eventWithCategory: .shortcuts, name: "support") - default: - break - } - - // reset the shortcut item - shortcutItemToProcess = nil - } - } - - func refreshCacheScanLibraryAndUpload(preload: Bool, isSwitching: Bool) { - Log.appDelegate("refreshCacheScanLibraryAndUpload preload:\(preload) isSwitching:\(preload)") - - guard let currentAccount = accountManager.currentAccount else { - Log.appDelegate("No account to refresh", level: .error) - return - } - - let rootViewController = window?.rootViewController as? UpdateAccountDelegate - - availableOfflineManager.updateAvailableOfflineFiles(status: ReachabilityListener.instance.currentStatus) - - Task { - do { - let oldDriveId = accountManager.currentDriveFileManager?.drive.objectId - let account = try await accountManager.updateUser(for: currentAccount, registerToken: true) - rootViewController?.didUpdateCurrentAccountInformations(account) - - if let oldDriveId, - let newDrive = driveInfosManager.getDrive(primaryKey: oldDriveId), - !newDrive.inMaintenance { - // The current drive is still usable, do not switch - scanLibraryAndRestartUpload() - return - } - - let driveFileManager = try accountManager.getFirstAvailableDriveFileManager(for: account.userId) - accountManager.setCurrentDriveForCurrentAccount(drive: driveFileManager.drive) - showMainViewController(driveFileManager: driveFileManager) - scanLibraryAndRestartUpload() - } catch DriveError.NoDriveError.noDrive { - let driveErrorNavigationViewController = DriveErrorViewController.instantiateInNavigationController( - errorType: .noDrive, - drive: nil - ) - setRootViewController(driveErrorNavigationViewController) - } catch DriveError.NoDriveError.blocked(let drive), DriveError.NoDriveError.maintenance(let drive) { - let driveErrorNavigationViewController = DriveErrorViewController.instantiateInNavigationController( - errorType: drive.isInTechnicalMaintenance ? .maintenance : .blocked, - drive: drive - ) - setRootViewController(driveErrorNavigationViewController) - } catch { - UIConstants.showSnackBarIfNeeded(error: DriveError.unknownError) - Log.appDelegate("Error while updating user account: \(error)", level: .error) - } - } - } - - private func scanLibraryAndRestartUpload() { - // Resolving an upload queue will restart it if this is the first time - @InjectService var uploadQueue: UploadQueue - - backgroundUploadSessionManager.reconnectBackgroundTasks() - DispatchQueue.global(qos: .utility).async { - Log.appDelegate("Restart queue") - @InjectService var photoUploader: PhotoLibraryUploader - _ = photoUploader.scheduleNewPicturesForUpload() - - @InjectService var uploadQueue: UploadQueue - uploadQueue.rebuildUploadQueueFromObjectsInRealm() - } - } - func application(_ application: UIApplication, open url: URL, sourceApplication: String?, @@ -354,112 +153,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg return infomaniakLogin.handleRedirectUri(url: url) } - func setRootViewController(_ vc: UIViewController, - animated: Bool = true) { - 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, - completion: nil) - } - - func present(file: File, driveFileManager: DriveFileManager, office: Bool = false) { - guard let rootViewController = window?.rootViewController as? MainTabViewController else { - return - } - - // Dismiss all view controllers presented - rootViewController.dismiss(animated: false) { - // Select Files tab - rootViewController.selectedIndex = 1 - - guard let navController = rootViewController.selectedViewController as? UINavigationController, - let viewController = navController.topViewController as? FileListViewController else { - return - } - - guard !file.isRoot, - viewController.viewModel.currentDirectory.id != file.id else { - Log.appDelegate("Already presenting the correct screen") - return - } - - // Pop to root - navController.popToRootViewController(animated: false) - - // Present file - guard let fileListViewController = navController.topViewController as? RootMenuViewController else { - Log.appDelegate("Top navigation is not a RootMenuViewController") - return - } - - if office { - OnlyOfficeViewController.open(driveFileManager: driveFileManager, - file: file, - viewController: fileListViewController) - } else { - let filePresenter = FilePresenter(viewController: fileListViewController) - filePresenter.present(for: file, - files: [file], - driveFileManager: driveFileManager, - normalFolderHierarchy: false) - } - } - } - - @objc func handleLocateUploadNotification(_ notification: Notification) { - guard let parentId = notification.userInfo?["parentId"] as? Int else { - Log.appDelegate("No parentId") - return - } - - guard let driveFileManager = accountManager.currentDriveFileManager else { - Log.appDelegate("No driveFileManager") - return - } - - guard let folder = driveFileManager.getCachedFile(id: parentId) else { - Log.appDelegate("No matching cached files") - return - } - - present(file: folder, driveFileManager: driveFileManager) - } - - @objc func reloadDrive(_ notification: Notification) { - Task { @MainActor in - self.refreshCacheScanLibraryAndUpload(preload: false, isSwitching: false) - } - } - - // MARK: - Account manager delegate - - func currentAccountNeedsAuthentication() { - Task { @MainActor in - setRootViewController(SwitchUserViewController.instantiateInNavigationController()) - } - } - - // MARK: - State restoration - - func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { - return appRestorationService.shouldSaveApplicationState(coder: coder) - } - - func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { - return appRestorationService.shouldRestoreApplicationState(coder: coder) - } - // MARK: - User activity func application(_ application: UIApplication, @@ -487,7 +180,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { _ = notification.request.content.userInfo - // Change this to your preferred presentation option completionHandler([.alert, .sound]) } @@ -510,7 +202,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { if let parentId, let driveFileManager = accountManager.currentDriveFileManager, let folder = driveFileManager.getCachedFile(id: parentId) { - present(file: folder, driveFileManager: driveFileManager) + appNavigable.present(file: folder, driveFileManager: driveFileManager) } default: break @@ -518,22 +210,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } else if response.notification.request.content.categoryIdentifier == NotificationsHelper.CategoryIdentifier .photoSyncError { // Show photo sync settings - guard let rootViewController = window?.rootViewController as? MainTabViewController else { - return - } - - // Dismiss all view controllers presented - rootViewController.dismiss(animated: false) - // Select Menu tab - rootViewController.selectedIndex = 4 - - guard let navController = rootViewController.selectedViewController as? UINavigationController else { - return - } - - let photoSyncSettingsViewController = PhotoSyncSettingsViewController.instantiate() - navController.popToRootViewController(animated: false) - navController.pushViewController(photoSyncSettingsViewController, animated: true) + appNavigable.showPhotoSyncSettings() } else { // Handle other notification types... } @@ -557,15 +234,3 @@ extension AppDelegate: UNUserNotificationCenterDelegate { // Messaging.messaging().appDidReceiveMessage(userInfo) } } - -// MARK: - Navigation - -extension AppDelegate { - var topMostViewController: UIViewController? { - var topViewController = window?.rootViewController - while let presentedViewController = topViewController?.presentedViewController { - topViewController = presentedViewController - } - return topViewController - } -} diff --git a/kDrive/AppRestorationService.swift b/kDrive/AppRestorationService.swift index b1ade3fac..b0ead6434 100644 --- a/kDrive/AppRestorationService.swift +++ b/kDrive/AppRestorationService.swift @@ -21,8 +21,23 @@ import InfomaniakDI import kDriveCore import UIKit -// TODO: Refactor with Scenes / NSUserActivity -public final class AppRestorationService { +/// Something that centralize the App Restoration logic +public protocol AppRestorationServiceable { + /// Is restoration enabled + var shouldRestoreApplicationState: Bool { get } + + /// Should save the scene sate + var shouldSaveApplicationState: Bool { get } + + /// Saves a restoration version, for forward compatibility + func saveRestorationVersion() + + func reloadAppUI(for driveId: Int, userId: Int) async +} + +public final class AppRestorationService: AppRestorationServiceable { + @LazyInjectService var appNavigable: AppNavigable + /// Path where the state restoration state is saved private static let statePath = FileManager.default .urls(for: .libraryDirectory, in: .userDomainMask) @@ -32,34 +47,35 @@ public final class AppRestorationService { @LazyInjectService private var accountManager: AccountManageable /// State restoration version - private static let currentStateVersion = 4 - - /// State restoration key - private static let appStateVersionKey = "appStateVersionKey" + private static let currentStateVersion = 5 public init() { // META: keep SonarCloud happy } - public func shouldSaveApplicationState(coder: NSCoder) -> Bool { - Log.appDelegate("shouldSaveApplicationState") - Log.appDelegate("Restoration files:\(String(describing: Self.statePath))") - coder.encode(Self.currentStateVersion, forKey: Self.appStateVersionKey) + public var shouldSaveApplicationState: Bool { + Log.sceneDelegate("shouldSaveApplicationState") + Log.sceneDelegate("Restoration files:\(String(describing: Self.statePath))") + return true } - public func shouldRestoreApplicationState(coder: NSCoder) -> Bool { - return false - /* TODO: Rework app restoration before re-enabling - let encodedVersion = coder.decodeInteger(forKey: Self.appStateVersionKey) - let shouldRestoreApplicationState = Self.currentStateVersion == encodedVersion && - !(UserDefaults.shared.legacyIsFirstLaunch || accountManager.accounts.isEmpty) - Log.appDelegate("shouldRestoreApplicationState:\(shouldRestoreApplicationState)") - return shouldRestoreApplicationState*/ + public var shouldRestoreApplicationState: Bool { + let storedVersion = UserDefaults.shared.appRestorationVersion + let shouldRestore = Self.currentStateVersion == storedVersion && + !(UserDefaults.shared.legacyIsFirstLaunch || accountManager.accounts.isEmpty) + + Log.sceneDelegate("shouldRestoreApplicationState:\(shouldRestore) appRestorationVersion:\(storedVersion)") + return shouldRestore + } + + public func saveRestorationVersion() { + UserDefaults.shared.appRestorationVersion = Self.currentStateVersion + Log.sceneDelegate("saveRestorationVersion to \(Self.currentStateVersion)") } - public func reloadAppUI(for drive: Drive) { - accountManager.setCurrentDriveForCurrentAccount(drive: drive) + public func reloadAppUI(for driveId: Int, userId: Int) async { + accountManager.setCurrentDriveForCurrentAccount(for: driveId, userId: userId) accountManager.saveAccounts() guard let currentDriveFileManager = accountManager.currentDriveFileManager else { @@ -67,12 +83,8 @@ public final class AppRestorationService { } // Read the last tab selected in order to properly reload the App's UI. - // This should be migrated to NSUserActivity at some point let lastSelectedTab = UserDefaults.shared.lastSelectedTab - let newMainTabViewController = MainTabViewController( - driveFileManager: currentDriveFileManager, - selectedIndex: lastSelectedTab - ) - (UIApplication.shared.delegate as? AppDelegate)?.setRootViewController(newMainTabViewController) + + await appNavigable.showMainViewController(driveFileManager: currentDriveFileManager, selectedIndex: lastSelectedTab) } } diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift new file mode 100644 index 000000000..59783ad93 --- /dev/null +++ b/kDrive/AppRouter.swift @@ -0,0 +1,772 @@ +/* + 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 InfomaniakCore +import InfomaniakCoreUI +import InfomaniakDI +import kDriveCore +import kDriveResources +import SafariServices +import UIKit +import VersionChecker + +/// Something that can navigate to specific places of the kDrive app +public protocol RouterAppNavigable { + /// Show the main view with a customizable selected index + /// - Parameters: + /// - driveFileManager: driveFileManager to use + /// - selectedIndex: Nil will try to use state restoration if available + @MainActor func showMainViewController(driveFileManager: DriveFileManager, selectedIndex: Int?) -> UITabBarController? + + @MainActor func showPreloading(currentAccount: Account) + + @MainActor func showOnboarding() + + @MainActor func showAppLock() + + @MainActor func showLaunchFloatingPanel() + + @MainActor func showUpdateRequired() + + @MainActor func showPhotoSyncSettings() +} + +/// Something that can present a File within the app +public protocol RouterFileNavigable { + /// Pop to root and present file, will never open OnlyOffice + /// - Parameters: + /// - file: File to display + /// - driveFileManager: driveFileManager + @MainActor func present(file: File, driveFileManager: DriveFileManager) + + /// Pop to root and present file + /// - Parameters: + /// - file: File to display + /// - driveFileManager: driveFileManager + /// - office: Open in only office + @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) + + /// Present a list of files from a folder + /// - Parameters: + /// - frozenFolder: Folder to display + /// - driveFileManager: driveFileManager + /// - navigationController: The navigation controller to use + @MainActor func presentFileList( + frozenFolder: File, + driveFileManager: DriveFileManager, + navigationController: UINavigationController + ) + + /// Present PreviewViewController + /// - Parameters: + /// - frozenFiles: File list to display, must be frozen + /// - index: The Index of the file to display + /// - driveFileManager: The driveFileManager + /// - normalFolderHierarchy: See FileListViewModel.Configuration for details + /// - fromActivities: Opening from an activity + /// - navigationController: The navigation controller to use + /// - animated: Should be animated + @MainActor func presentPreviewViewController( + frozenFiles: [File], + index: Int, + driveFileManager: DriveFileManager, + normalFolderHierarchy: Bool, + fromActivities: Bool, + navigationController: UINavigationController, + animated: Bool + ) + + /// Present the details of a file and all the linked metadata + /// - Parameters: + /// - frozenFile: A frozen file to display + /// - driveFileManager: driveFileManager + /// - navigationController: The navigation controller to use + /// - animated: Should be animated + @MainActor func presentFileDetails( + frozenFile: File, + driveFileManager: DriveFileManager, + navigationController: UINavigationController, + animated: Bool + ) + + /// Present the InApp purchase StoreViewController + /// - Parameters: + /// - driveFileManager: driveFileManager + /// - navigationController: The navigation controller to use + /// - animated: Should be animated + @MainActor func presentStoreViewController( + driveFileManager: DriveFileManager, + navigationController: UINavigationController, + animated: Bool + ) +} + +/// Something that can set an arbitrary RootView controller +public protocol RouterRootNavigable { + /// Something that can set an arbitrary RootView controller + /// + /// Should not be used externally except by SceneDelegate. + @MainActor func setRootViewController(_ viewController: UIViewController, + animated: Bool) + + /// Setup the root of the view stack + /// - Parameters: + /// - currentState: the state to present + /// - restoration: try to restore scene or not + @MainActor func prepareRootViewController(currentState: RootViewControllerState, restoration: Bool) + + /// Set the main theme color + @MainActor func updateTheme() +} + +public protocol TopmostViewControllerFetchable { + /// Access the current top most ViewController + @MainActor var topMostViewController: UIViewController? { get } +} + +/// Actions performed by router, `async` by design +public protocol RouterActionable { + /// Ask the user to review the app + func askForReview() async + + /// Ask the user to remove pictures if configured + func askUserToRemovePicturesIfNecessary() async + + func refreshCacheScanLibraryAndUpload(preload: Bool, isSwitching: Bool) async +} + +/// Something that can navigate within the kDrive app +public typealias AppNavigable = RouterActionable + & RouterAppNavigable + & RouterFileNavigable + & RouterRootNavigable + & TopmostViewControllerFetchable + +public struct AppRouter: AppNavigable { + @LazyInjectService private var appRestorationService: AppRestorationServiceable + @LazyInjectService private var driveInfosManager: DriveInfosManager + @LazyInjectService private var keychainHelper: KeychainHelper + @LazyInjectService private var reviewManager: ReviewManageable + @LazyInjectService private var availableOfflineManager: AvailableOfflineManageable + @LazyInjectService private var backgroundUploadSessionManager: BackgroundUploadSessionManager + @LazyInjectService private var accountManager: AccountManageable + + /// Get the current window from the app scene + @MainActor private var window: UIWindow? { + let scene = UIApplication.shared.connectedScenes.first { scene in + guard let delegate = scene.delegate, + delegate as? SceneDelegate != nil else { + return false + } + + return true + } + + guard let sceneDelegate = scene?.delegate as? SceneDelegate, + let window = sceneDelegate.window else { + return nil + } + + return window + } + + @MainActor var sceneUserInfo: [AnyHashable: Any]? { + guard let scene = window?.windowScene, + let userInfo = scene.userActivity?.userInfo else { + return nil + } + + return userInfo + } + + // MARK: TopmostViewControllerFetchable + + @MainActor public var topMostViewController: UIViewController? { + var topViewController = window?.rootViewController + while let presentedViewController = topViewController?.presentedViewController { + topViewController = presentedViewController + } + return topViewController + } + + // MARK: RouterRootNavigable + + @MainActor public func setRootViewController(_ viewController: UIViewController, + animated: Bool) { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + window.rootViewController = viewController + window.makeKeyAndVisible() + + guard animated else { + return + } + + UIView.transition(with: window, duration: 0.3, + options: .transitionCrossDissolve, + animations: nil, + completion: nil) + } + + @MainActor public func prepareRootViewController(currentState: RootViewControllerState, restoration: Bool) { + switch currentState { + case .appLock: + showAppLock() + case .mainViewController(let driveFileManager): + + restoreMainUIStackIfPossible(driveFileManager: driveFileManager, restoration: restoration) + + showLaunchFloatingPanel() + Task { + await askForReview() + await askUserToRemovePicturesIfNecessary() + } + case .onboarding: + showOnboarding() + case .updateRequired: + showUpdateRequired() + case .preloading(let currentAccount): + showPreloading(currentAccount: currentAccount) + } + } + + /// Entry point for scene restoration + @MainActor func restoreMainUIStackIfPossible(driveFileManager: DriveFileManager, restoration: Bool) { + let shouldRestoreApplicationState = appRestorationService.shouldRestoreApplicationState + var indexToUse: Int? + if shouldRestoreApplicationState, + let sceneUserInfo, + let index = sceneUserInfo[SceneRestorationKeys.selectedIndex.rawValue] as? Int { + indexToUse = index + } + + let tabBarViewController = showMainViewController(driveFileManager: driveFileManager, selectedIndex: indexToUse) + + guard shouldRestoreApplicationState else { + Log.sceneDelegate("Restoration disabled", level: .error) + appRestorationService.saveRestorationVersion() + return + } + + Task { @MainActor in + guard restoration, let tabBarViewController else { + return + } + + guard let sceneUserInfo, + let lastViewControllerString = sceneUserInfo[SceneRestorationKeys.lastViewController.rawValue] as? String, + let lastViewController = SceneRestorationScreens(rawValue: lastViewControllerString) else { + return + } + + let selectedIndex = tabBarViewController.selectedIndex + let viewControllers = tabBarViewController.viewControllers + guard let rootNavigationController = viewControllers?[safe: selectedIndex] as? UINavigationController else { + Log.sceneDelegate("unable to access navigationController", level: .error) + return + } + + switch lastViewController { + case .FileDetailViewController: + await restoreFileDetailViewController( + driveFileManager: driveFileManager, + navigationController: rootNavigationController, + sceneUserInfo: sceneUserInfo + ) + + case .FileListViewController: + await restoreFileListViewController( + driveFileManager: driveFileManager, + navigationController: rootNavigationController, + sceneUserInfo: sceneUserInfo + ) + + case .PreviewViewController: + await restorePreviewViewController( + driveFileManager: driveFileManager, + navigationController: rootNavigationController, + sceneUserInfo: sceneUserInfo + ) + + case .StoreViewController: + await restoreStoreViewController( + driveFileManager: driveFileManager, + navigationController: rootNavigationController, + sceneUserInfo: sceneUserInfo + ) + } + } + } + + private func restoreFileDetailViewController(driveFileManager: DriveFileManager, + navigationController: UINavigationController, + sceneUserInfo: [AnyHashable: Any]) async { + guard let fileId = sceneUserInfo[SceneRestorationValues.fileId.rawValue] else { + Log.sceneDelegate("unable to load file id", level: .error) + return + } + + let database = driveFileManager.database + let frozenFile = database.fetchObject(ofType: File.self) { lazyCollection in + lazyCollection + .filter("id == %@", fileId) + .first? + .freezeIfNeeded() + } + + guard let frozenFile else { + Log.sceneDelegate("unable to load file", level: .error) + return + } + + await presentFileDetails(frozenFile: frozenFile, + driveFileManager: driveFileManager, + navigationController: navigationController, + animated: false) + } + + private func restoreFileListViewController(driveFileManager: DriveFileManager, + navigationController: UINavigationController, + sceneUserInfo: [AnyHashable: Any]) async { + guard let driveId = sceneUserInfo[SceneRestorationValues.driveId.rawValue] as? Int, + driveFileManager.drive.id == driveId, + let fileId = sceneUserInfo[SceneRestorationValues.fileId.rawValue] else { + Log.sceneDelegate("metadata issue for FileList :\(sceneUserInfo)", level: .error) + return + } + + let database = driveFileManager.database + let frozenFile = database.fetchObject(ofType: File.self) { lazyCollection in + lazyCollection + .filter("id == %@", fileId) + .first? + .freezeIfNeeded() + } + + guard let frozenFile else { + Log.sceneDelegate("unable to load file", level: .error) + return + } + + await presentFileList(frozenFolder: frozenFile, + driveFileManager: driveFileManager, + navigationController: navigationController) + } + + private func restorePreviewViewController(driveFileManager: DriveFileManager, + navigationController: UINavigationController, + sceneUserInfo: [AnyHashable: Any]) async { + guard sceneUserInfo[SceneRestorationValues.driveId.rawValue] as? Int != nil, + let fileIds = sceneUserInfo[SceneRestorationValues.Carousel.filesIds.rawValue] as? [Int], + let currentIndex = sceneUserInfo[SceneRestorationValues.Carousel.currentIndex.rawValue] as? Int, + let normalFolderHierarchy = sceneUserInfo[SceneRestorationValues.Carousel.normalFolderHierarchy.rawValue] as? Bool, + let fromActivities = sceneUserInfo[SceneRestorationValues.Carousel.fromActivities.rawValue] as? Bool else { + Log.sceneDelegate("metadata issue for PreviewController :\(sceneUserInfo)", level: .error) + return + } + + let database = driveFileManager.database + let frozenFetchedFiles = database.fetchResults(ofType: File.self) { lazyCollection in + lazyCollection + .filter("id IN %@", fileIds) + .freezeIfNeeded() + } + + let frozenFilesToRestore = Array(frozenFetchedFiles) + + await presentPreviewViewController( + frozenFiles: frozenFilesToRestore, + index: currentIndex, + driveFileManager: driveFileManager, + normalFolderHierarchy: normalFolderHierarchy, + fromActivities: fromActivities, + navigationController: navigationController, + animated: false + ) + } + + private func restoreStoreViewController(driveFileManager: DriveFileManager, + navigationController: UINavigationController, + sceneUserInfo: [AnyHashable: Any]) async { + guard let driveId = sceneUserInfo[SceneRestorationValues.driveId.rawValue] as? Int, + driveFileManager.drive.id == driveId else { + Log.sceneDelegate("unable to load drive id", level: .error) + return + } + + await presentStoreViewController( + driveFileManager: driveFileManager, + navigationController: navigationController, + animated: false + ) + } + + @MainActor public func updateTheme() { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + window.overrideUserInterfaceStyle = UserDefaults.shared.theme.interfaceStyle + } + + // MARK: RouterAppNavigable + + @discardableResult + @MainActor public func showMainViewController(driveFileManager: DriveFileManager, + selectedIndex: Int?) -> UITabBarController? { + guard let window else { + SentryDebug.captureNoWindow() + return nil + } + + let currentDriveObjectId = (window.rootViewController as? MainTabViewController)?.driveFileManager.drive.objectId + guard currentDriveObjectId != driveFileManager.drive.objectId else { + return nil + } + + let tabBarViewController = MainTabViewController(driveFileManager: driveFileManager, + selectedIndex: selectedIndex) + + window.rootViewController = tabBarViewController + window.makeKeyAndVisible() + + return tabBarViewController + } + + @MainActor public func showPreloading(currentAccount: Account) { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + window.rootViewController = PreloadingViewController(currentAccount: currentAccount) + window.makeKeyAndVisible() + } + + @MainActor public func showOnboarding() { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + defer { + // Clean File Provider domains on first launch in case we had some dangling + driveInfosManager.deleteAllFileProviderDomains() + } + + let isNotPresentingOnboarding = window.rootViewController?.isKind(of: OnboardingViewController.self) != true + guard isNotPresentingOnboarding else { + return + } + + keychainHelper.deleteAllTokens() + window.rootViewController = OnboardingViewController.instantiate() + window.makeKeyAndVisible() + } + + @MainActor public func showAppLock() { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + window.rootViewController = LockedAppViewController.instantiate() + window.makeKeyAndVisible() + } + + @MainActor public func showLaunchFloatingPanel() { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + let launchPanelsController = LaunchPanelsController() + if let viewController = window.rootViewController { + launchPanelsController.pickAndDisplayPanel(viewController: viewController) + } + } + + @MainActor public func showUpdateRequired() { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + window.rootViewController = DriveUpdateRequiredViewController() + window.makeKeyAndVisible() + } + + @MainActor public func showPhotoSyncSettings() { + guard let rootViewController = window?.rootViewController as? MainTabViewController else { + return + } + + rootViewController.dismiss(animated: false) + rootViewController.selectedIndex = MainTabIndex.profile.rawValue + + guard let navController = rootViewController.selectedViewController as? UINavigationController else { + return + } + + let photoSyncSettingsViewController = PhotoSyncSettingsViewController.instantiate() + navController.popToRootViewController(animated: false) + navController.pushViewController(photoSyncSettingsViewController, animated: true) + } + + // MARK: RouterActionable + + public func askUserToRemovePicturesIfNecessary() async { + @InjectService var photoCleaner: PhotoLibraryCleanerServiceable + guard photoCleaner.hasPicturesToRemove else { + Log.sceneDelegate("No pictures to remove", level: .info) + return + } + + Task { @MainActor in + let alert = AlertTextViewController(title: KDriveResourcesStrings.Localizable.modalDeletePhotosTitle, + message: KDriveResourcesStrings.Localizable.modalDeletePhotosDescription, + action: KDriveResourcesStrings.Localizable.buttonDelete, + destructive: true, + loading: false) { + Task { + @InjectService var photoCleaner: PhotoLibraryCleanerServiceable + await photoCleaner.removePicturesScheduledForDeletion() + } + } + + window?.rootViewController?.present(alert, animated: true) + } + } + + public func askForReview() async { + guard let presentingViewController = await window?.rootViewController, + !Bundle.main.isRunningInTestFlight else { + return + } + + guard reviewManager.shouldRequestReview() else { + return + } + + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String + + Task { @MainActor in + let alert = AlertTextViewController( + title: appName, + message: KDriveResourcesStrings.Localizable.reviewAlertTitle, + action: KDriveResourcesStrings.Localizable.buttonYes, + hasCancelButton: true, + cancelString: KDriveResourcesStrings.Localizable.buttonNo, + handler: requestAppStoreReview, + cancelHandler: openUserReport + ) + + presentingViewController.present(alert, animated: true) + } + MatomoUtils.track(eventWithCategory: .appReview, name: "alertPresented") + } + + @MainActor private func requestAppStoreReview() { + MatomoUtils.track(eventWithCategory: .appReview, name: "like") + UserDefaults.shared.appReview = .readyForReview + reviewManager.requestReview() + } + + @MainActor private func openUserReport() { + MatomoUtils.track(eventWithCategory: .appReview, name: "dislike") + guard let url = URL(string: KDriveResourcesStrings.Localizable.urlUserReportiOS), + let presentingViewController = window?.rootViewController else { + return + } + UserDefaults.shared.appReview = .feedback + presentingViewController.present(SFSafariViewController(url: url), animated: true) + } + + public func refreshCacheScanLibraryAndUpload(preload: Bool, isSwitching: Bool) async { + Log.sceneDelegate("refreshCacheScanLibraryAndUpload preload:\(preload) isSwitching:\(preload)") + + availableOfflineManager.updateAvailableOfflineFiles(status: ReachabilityListener.instance.currentStatus) + + do { + try await refreshAccountAndShowMainView() + await scanLibraryAndRestartUpload() + } catch DriveError.NoDriveError.noDrive { + let driveErrorNavigationViewController = await DriveErrorViewController.instantiateInNavigationController( + errorType: .noDrive, + drive: nil + ) + await setRootViewController(driveErrorNavigationViewController, animated: true) + } catch DriveError.NoDriveError.blocked(let drive), DriveError.NoDriveError.maintenance(let drive) { + let driveErrorNavigationViewController = await DriveErrorViewController.instantiateInNavigationController( + errorType: drive.isInTechnicalMaintenance ? .maintenance : .blocked, + drive: drive + ) + await setRootViewController(driveErrorNavigationViewController, animated: true) + } catch { + await UIConstants.showSnackBarIfNeeded(error: DriveError.unknownError) + Log.sceneDelegate("Error while updating user account: \(error)", level: .error) + } + } + + @MainActor private func refreshAccountAndShowMainView() async throws { + let oldDriveId = accountManager.currentDriveFileManager?.drive.objectId + + guard let currentAccount = accountManager.currentAccount else { + Log.sceneDelegate("No account to refresh", level: .error) + return + } + + let account = try await accountManager.updateUser(for: currentAccount, registerToken: true) + let rootViewController = window?.rootViewController as? UpdateAccountDelegate + rootViewController?.didUpdateCurrentAccountInformations(account) + + if let oldDriveId, + let newDrive = driveInfosManager.getDrive(primaryKey: oldDriveId), + !newDrive.inMaintenance { + // The current drive is still usable, do not switch + await scanLibraryAndRestartUpload() + return + } + + let driveFileManager = try accountManager.getFirstAvailableDriveFileManager(for: account.userId) + let drive = driveFileManager.drive + accountManager.setCurrentDriveForCurrentAccount(for: drive.id, userId: drive.userId) + showMainViewController(driveFileManager: driveFileManager, selectedIndex: nil) + } + + private func scanLibraryAndRestartUpload() async { + backgroundUploadSessionManager.reconnectBackgroundTasks() + + Log.sceneDelegate("Restart queue") + @InjectService var photoUploader: PhotoLibraryUploader + photoUploader.scheduleNewPicturesForUpload() + + // Resolving an upload queue will restart it if this is the first time + @InjectService var uploadQueue: UploadQueue + uploadQueue.rebuildUploadQueueFromObjectsInRealm() + } + + // MARK: RouterFileNavigable + + @MainActor public func present(file: File, driveFileManager: DriveFileManager) { + present(file: file, driveFileManager: driveFileManager, office: false) + } + + @MainActor public func present(file: File, driveFileManager: DriveFileManager, office: Bool) { + guard let rootViewController = window?.rootViewController as? MainTabViewController else { + return + } + + rootViewController.dismiss(animated: false) { + rootViewController.selectedIndex = MainTabIndex.files.rawValue + + guard let navController = rootViewController.selectedViewController as? UINavigationController, + let viewController = navController.topViewController as? FileListViewController else { + return + } + + guard !file.isRoot && viewController.viewModel.currentDirectory.id != file.id else { + return + } + + navController.popToRootViewController(animated: false) + + guard let fileListViewController = navController.topViewController as? FileListViewController else { + return + } + + if office { + OnlyOfficeViewController.open(driveFileManager: driveFileManager, + file: file, + viewController: fileListViewController) + } else { + let filePresenter = FilePresenter(viewController: fileListViewController) + filePresenter.present(for: file, + files: [file], + driveFileManager: driveFileManager, + normalFolderHierarchy: false) + } + } + } + + @MainActor public func presentFileList( + frozenFolder: File, + driveFileManager: DriveFileManager, + navigationController: UINavigationController + ) { + assert(frozenFolder.realm == nil || frozenFolder.isFrozen, "expecting this realm object to be thread safe") + assert(frozenFolder.isDirectory, "This will only work for folders") + + guard let topViewController = navigationController.topViewController else { + Log.sceneDelegate("unable to presentFileList, no topViewController", level: .error) + return + } + + FilePresenter(viewController: topViewController) + .presentDirectory(for: frozenFolder, + driveFileManager: driveFileManager, + animated: false, + completion: nil) + } + + @MainActor public func presentPreviewViewController( + frozenFiles: [File], + index: Int, + driveFileManager: DriveFileManager, + normalFolderHierarchy: Bool, + fromActivities: Bool, + navigationController: UINavigationController, + animated: Bool + ) { + let previewViewController = PreviewViewController.instantiate(files: frozenFiles, + index: index, + driveFileManager: driveFileManager, + normalFolderHierarchy: normalFolderHierarchy, + fromActivities: fromActivities) + navigationController.pushViewController(previewViewController, animated: animated) + } + + @MainActor public func presentFileDetails( + frozenFile: File, + driveFileManager: DriveFileManager, + navigationController: UINavigationController, + animated: Bool + ) { + assert(frozenFile.realm == nil || frozenFile.isFrozen, "expecting this realm object to be thread safe") + + let fileDetailViewController = FileDetailViewController.instantiate( + driveFileManager: driveFileManager, + file: frozenFile + ) + + navigationController.pushViewController(fileDetailViewController, animated: animated) + } + + @MainActor public func presentStoreViewController( + driveFileManager: DriveFileManager, + navigationController: UINavigationController, + animated: Bool + ) { + let storeViewController = StoreViewController.instantiate(driveFileManager: driveFileManager) + navigationController.pushViewController(storeViewController, animated: false) + } +} diff --git a/kDrive/Resources/Info.plist b/kDrive/Resources/Info.plist index 1de821357..d34031e6b 100644 --- a/kDrive/Resources/Info.plist +++ b/kDrive/Resources/Info.plist @@ -91,6 +91,31 @@ To be able to record movies you must allow the use of the microphone NSPhotoLibraryAddUsageDescription Save picture + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).mainActivity + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UILaunchStoryboardName + LaunchScreen + UISceneClassName + UIWindowScene + UISceneConfigurationName + Main Scene + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + NSPhotoLibraryUsageDescription Auto upload PHPhotoLibraryPreventAutomaticLimitedAccessAlert diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift new file mode 100644 index 000000000..ec8dbb699 --- /dev/null +++ b/kDrive/SceneDelegate.swift @@ -0,0 +1,354 @@ +/* + 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 InfomaniakCore +import InfomaniakCoreUI +import InfomaniakDI +import kDriveCore +import kDriveResources +import SafariServices +import UIKit +import VersionChecker + +final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate { + @LazyInjectService var lockHelper: AppLockHelper + @LazyInjectService var accountManager: AccountManageable + @LazyInjectService var driveInfosManager: DriveInfosManager + @LazyInjectService var backgroundTasksService: BackgroundTasksServiceable + @LazyInjectService var appNavigable: AppNavigable + @LazyInjectService var appRestorationService: AppRestorationServiceable + + var shortcutItemToProcess: UIApplicationShortcutItem? + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + Log.sceneDelegate("scene session options") + guard let windowScene = (scene as? UIWindowScene) else { + return + } + + if let shortcutItem = connectionOptions.shortcutItem { + shortcutItemToProcess = shortcutItem + } + + prepareWindowScene(windowScene) + + accountManager.delegate = self + + NotificationCenter.default.addObserver(self, selector: #selector(reloadDrive), name: .reloadDrive, object: nil) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleLocateUploadNotification), + name: .locateUploadActionTapped, + object: nil + ) + + let isRestoration: Bool = session.stateRestorationActivity != nil + Log.sceneDelegate("user activity isRestoration:\(isRestoration) \(session.stateRestorationActivity)") + + guard let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity else { + Log.sceneDelegate("no user activity") + return + } + + guard userActivity.activityType == SceneActivityIdentifier.mainSceneActivityType else { + Log.sceneDelegate("unsupported user activity type:\(userActivity.activityType)") + return + } + + scene.userActivity = userActivity + + guard let userInfo = userActivity.userInfo else { + Log.sceneDelegate("activity has no metadata to process") + return + } + + Log.sceneDelegate("restore from \(userActivity.activityType)") + Log.sceneDelegate("selectedIndex:\(userInfo[SceneRestorationKeys.selectedIndex.rawValue])") + } + + private func prepareWindowScene(_ windowScene: UIWindowScene) { + let newWindow = UIWindow(windowScene: windowScene) + + window = newWindow + newWindow.makeKeyAndVisible() + + setGlobalWindowTint() + appNavigable.updateTheme() + } + + func configure(window: UIWindow?, session: UISceneSession, with activity: NSUserActivity) -> Bool { + Log.sceneDelegate("configure session with") + return true + } + + func sceneDidDisconnect(_ scene: UIScene) { + Log.sceneDelegate("sceneDidDisconnect \(scene)") + } + + func sceneWillResignActive(_ scene: UIScene) { + Log.sceneDelegate("sceneWillResignActive \(scene)") + } + + func sceneWillEnterForeground(_ scene: UIScene) { + Log.sceneDelegate("sceneWillEnterForeground \(scene) \(window)") + @InjectService var uploadQueue: UploadQueue + uploadQueue.pausedNotificationSent = false + + let currentState = RootViewControllerState.getCurrentState() + let session = scene.session + let isRestoration: Bool = session.stateRestorationActivity != nil + Log.sceneDelegate("user activity isRestoration:\(isRestoration) \(session.stateRestorationActivity)") + appNavigable.prepareRootViewController(currentState: currentState, restoration: isRestoration) + + switch currentState { + case .mainViewController, .appLock: + UserDefaults.shared.numberOfConnections += 1 + UserDefaults.shared.openingUntilReview -= 1 + Task { + await appNavigable.refreshCacheScanLibraryAndUpload(preload: false, isSwitching: false) + } + uploadEditedFiles() + case .onboarding, .updateRequired, .preloading: break + } + + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + + Task { + if try await VersionChecker.standard.checkAppVersionStatus() == .updateIsRequired { + appNavigable.prepareRootViewController(currentState: .updateRequired, restoration: false) + } + } + } + + func sceneDidBecomeActive(_ scene: UIScene) { + Log.sceneDelegate("sceneDidBecomeActive \(scene)") + guard let shortcutItem = shortcutItemToProcess else { + return + } + + guard let rootViewController = window?.rootViewController as? MainTabViewController else { + return + } + + rootViewController.dismiss(animated: false) + + guard let navController = rootViewController.selectedViewController as? UINavigationController, + let viewController = navController.topViewController, + let driveFileManager = accountManager.currentDriveFileManager else { + return + } + + switch shortcutItem.type { + case Constants.applicationShortcutScan: + let openMediaHelper = OpenMediaHelper(driveFileManager: driveFileManager) + openMediaHelper.openScan(rootViewController, false) + MatomoUtils.track(eventWithCategory: .shortcuts, name: "scan") + case Constants.applicationShortcutSearch: + let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager) + viewController.present( + SearchViewController.instantiateInNavigationController(viewModel: viewModel), + animated: true + ) + MatomoUtils.track(eventWithCategory: .shortcuts, name: "search") + case Constants.applicationShortcutUpload: + let openMediaHelper = OpenMediaHelper(driveFileManager: driveFileManager) + openMediaHelper.openMedia(rootViewController, .library) + MatomoUtils.track(eventWithCategory: .shortcuts, name: "upload") + case Constants.applicationShortcutSupport: + UIApplication.shared.open(URLConstants.support.url) + MatomoUtils.track(eventWithCategory: .shortcuts, name: "support") + default: + break + } + + shortcutItemToProcess = nil + } + + func sceneDidEnterBackground(_ scene: UIScene) { + Log.sceneDelegate("sceneDidEnterBackground \(scene)") + + backgroundTasksService.scheduleBackgroundRefresh() + + if UserDefaults.shared.isAppLockEnabled, + !(window?.rootViewController?.isKind(of: LockedAppViewController.self) ?? false) { + lockHelper.setTime() + } + } + + // MARK: - Window Scene + + func windowScene(_ windowScene: UIWindowScene, + didUpdate previousCoordinateSpace: UICoordinateSpace, + interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, + traitCollection previousTraitCollection: UITraitCollection) { + Log.sceneDelegate("windowScene didUpdate") + } + + func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem) async -> Bool { + Log.sceneDelegate("windowScene performActionFor :\(shortcutItem)") + shortcutItemToProcess = shortcutItem + return true + } + + // MARK: - Handoff support + + func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) { + Log.sceneDelegate("scene willContinueUserActivityWithType") + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + Log.sceneDelegate("scene continue userActivity") + } + + func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) { + Log.sceneDelegate("scene didFailToContinueUserActivityWithType") + } + + // MARK: - Account manager delegate + + func currentAccountNeedsAuthentication() { + Task { @MainActor in + let switchUser = SwitchUserViewController.instantiateInNavigationController() + appNavigable.setRootViewController(switchUser, animated: true) + } + } + + // MARK: - Reload drive notification + + @objc func reloadDrive(_ notification: Notification) { + Task { + await self.appNavigable.refreshCacheScanLibraryAndUpload(preload: false, isSwitching: false) + } + } + + @objc func handleLocateUploadNotification(_ notification: Notification) { + if let parentId = notification.userInfo?["parentId"] as? Int, + let driveFileManager = accountManager.currentDriveFileManager, + let folder = driveFileManager.getCachedFile(id: parentId) { + appNavigable.present(file: folder, driveFileManager: driveFileManager) + } + } +} + +// TODO: Refactor with router like pattern and split code away from this class +extension SceneDelegate { + func uploadEditedFiles() { + Log.sceneDelegate("uploadEditedFiles") + guard let folderURL = DriveFileManager.constants.openInPlaceDirectoryURL, + FileManager.default.fileExists(atPath: folderURL.path) else { + return + } + + let group = DispatchGroup() + var shouldCleanFolder = false + let driveFolders = (try? FileManager.default.contentsOfDirectory(atPath: folderURL.path)) ?? [] + // Hierarchy inside folderURL should be /driveId/fileId/fileName.extension + for driveFolder in driveFolders { + let driveFolderURL = folderURL.appendingPathComponent(driveFolder) + guard let driveId = Int(driveFolder), + let drive = driveInfosManager.getDrive(id: driveId, userId: accountManager.currentUserId), + let fileFolders = try? FileManager.default.contentsOfDirectory(atPath: driveFolderURL.path) else { + Log.sceneDelegate("[OPEN-IN-PLACE UPLOAD] Could not infer drive from \(driveFolderURL)") + continue + } + + for fileFolder in fileFolders { + let fileFolderURL = driveFolderURL.appendingPathComponent(fileFolder) + guard let fileId = Int(fileFolder), + let driveFileManager = accountManager.getDriveFileManager(for: drive.id, userId: drive.userId), + let file = driveFileManager.getCachedFile(id: fileId) else { + Log.sceneDelegate("[OPEN-IN-PLACE UPLOAD] Could not infer file from \(fileFolderURL)") + continue + } + + let fileURL = fileFolderURL.appendingPathComponent(file.name) + guard FileManager.default.fileExists(atPath: fileURL.path) else { + continue + } + + let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path) + let modificationDate = attributes?[.modificationDate] as? Date ?? Date(timeIntervalSince1970: 0) + + guard modificationDate > file.lastModifiedAt else { + continue + } + + let uploadFile = UploadFile(parentDirectoryId: file.parentId, + userId: accountManager.currentUserId, + driveId: file.driveId, + url: fileURL, + name: file.name, + conflictOption: .version, + shouldRemoveAfterUpload: false) + group.enter() + shouldCleanFolder = true + @InjectService var uploadQueue: UploadQueue + var observationToken: ObservationToken? + observationToken = uploadQueue + .observeFileUploaded(self, fileId: uploadFile.id) { [fileId = file.id] uploadFile, _ in + observationToken?.cancel() + if let error = uploadFile.error { + shouldCleanFolder = false + Log.sceneDelegate("[OPEN-IN-PLACE UPLOAD] Error while uploading: \(error)", level: .error) + } else { + // Update file to get the new modification date + Task { + let file = try await driveFileManager.file(id: fileId, forceRefresh: true) + try? FileManager.default.setAttributes([.modificationDate: file.lastModifiedAt], + ofItemAtPath: file.localUrl.path) + driveFileManager.notifyObserversWith(file: file) + } + } + group.leave() + } + uploadQueue.saveToRealm(uploadFile, itemIdentifier: nil) + } + } + + group.notify(queue: DispatchQueue.global(qos: .utility)) { + if shouldCleanFolder { + Log.sceneDelegate("[OPEN-IN-PLACE UPLOAD] Cleaning folder") + try? FileManager.default.removeItem(at: folderURL) + } + } + } + + private func setGlobalWindowTint() { + window?.tintColor = KDriveResourcesAsset.infomaniakColor.color + UITabBar.appearance().unselectedItemTintColor = KDriveResourcesAsset.iconColor.color + + // Migration from old UserDefaults + if UserDefaults.shared.legacyIsFirstLaunch { + UserDefaults.shared.legacyIsFirstLaunch = UserDefaults.standard.legacyIsFirstLaunch + } + } +} + +extension SceneDelegate { + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { + Log.sceneDelegate("stateRestorationActivity for:\(scene)") + guard appRestorationService.shouldRestoreApplicationState else { + return nil + } + + return scene.userActivity + } +} diff --git a/kDrive/UI/Controller/Base.lproj/Main.storyboard b/kDrive/UI/Controller/Base.lproj/Main.storyboard index 02ee1e3fe..ecfcd3e51 100644 --- a/kDrive/UI/Controller/Base.lproj/Main.storyboard +++ b/kDrive/UI/Controller/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -30,7 +30,7 @@ - + @@ -505,7 +505,7 @@ - + @@ -589,13 +589,13 @@ - + - + @@ -604,7 +604,7 @@ - + diff --git a/kDrive/UI/Controller/Files/Categories/EditCategoryViewController.swift b/kDrive/UI/Controller/Files/Categories/EditCategoryViewController.swift index 970a7d753..ed6584845 100644 --- a/kDrive/UI/Controller/Files/Categories/EditCategoryViewController.swift +++ b/kDrive/UI/Controller/Files/Categories/EditCategoryViewController.swift @@ -93,46 +93,6 @@ final class EditCategoryViewController: UITableViewController { return viewController } - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - if let categoryId = category?.id { - coder.encode(categoryId, forKey: "CategoryId") - } - if let filesIdToAdd = filesToAdd?.map(\.id) { - coder.encode(filesIdToAdd, forKey: "FilesId") - } - coder.encode(name, forKey: "Name") - coder.encode(color, forKey: "Color") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - let categoryId = coder.decodeInteger(forKey: "CategoryId") - let filesId = coder.decodeObject(of: [NSNumber.self], forKey: "FilesId") as? [NSNumber] - if let name = coder.decodeObject(of: NSString.self, forKey: "Name") { - self.name = name as String - } - if let color = coder.decodeObject(of: NSString.self, forKey: "Color") { - self.color = color as String - } - - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, userId: accountManager.currentUserId) else { - return - } - self.driveFileManager = driveFileManager - category = driveFileManager.drive.categories.first { $0.id == categoryId } - filesToAdd = filesId?.compactMap { driveFileManager.getCachedFile(id: Int(truncating: $0)) } - // Reload view - updateTitle() - setRows() - } - // MARK: - Table view data source override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { diff --git a/kDrive/UI/Controller/Files/Categories/ManageCategoriesViewController.swift b/kDrive/UI/Controller/Files/Categories/ManageCategoriesViewController.swift index 1f08da97b..4dbe36f03 100644 --- a/kDrive/UI/Controller/Files/Categories/ManageCategoriesViewController.swift +++ b/kDrive/UI/Controller/Files/Categories/ManageCategoriesViewController.swift @@ -230,41 +230,6 @@ final class ManageCategoriesViewController: UITableViewController { return UINavigationController(rootViewController: viewController) } - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - if let files { - coder.encode(files.map(\.id), forKey: "FilesId") - } - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - let filesId = coder.decodeObject(forKey: "FilesId") as! [Int] - - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, - userId: accountManager.currentUserId) else { - return - } - self.driveFileManager = driveFileManager - - let matchedFiles = driveFileManager.database.fetchResults(ofType: File.self) { lazyCollection in - lazyCollection.filter("id IN %@", filesId) - } - - files = Array(matchedFiles) - - // Reload view - updateTitle() - updateNavigationItem() - setUpObserver() - } - // MARK: - Table view data source override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { diff --git a/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift b/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift index 437bf98e1..9a191ebae 100644 --- a/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift +++ b/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift @@ -287,31 +287,6 @@ class ManageDropBoxViewController: UIViewController, UITableViewDelegate, UITabl } } } - - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - coder.encode(directory.id, forKey: "FolderId") - coder.encode(convertingFolder, forKey: "ConvertingFolder") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - let folderId = coder.decodeInteger(forKey: "FolderId") - let convertingFolder = coder.decodeBool(forKey: "ConvertingFolder") - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, - userId: accountManager.currentUserId) else { - return - } - self.driveFileManager = driveFileManager - self.convertingFolder = convertingFolder - directory = driveFileManager.getCachedFile(id: folderId) - } } // MARK: - NewFolderSettingsDelegate diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 104cadd55..bb04f202d 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -132,7 +132,7 @@ class ConcreteFileListViewModel: FileListViewModel { } class FileListViewController: UIViewController, UICollectionViewDataSource, SwipeActionCollectionViewDelegate, - SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate { + SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate, SceneStateRestorable { class var storyboard: UIStoryboard { Storyboard.files } class var storyboardIdentifier: String { "FileListViewController" } @@ -232,6 +232,8 @@ class FileListViewController: UIViewController, UICollectionViewDataSource, Swip override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) MatomoUtils.track(view: viewModel.configuration.matomoViewPath) + + saveSceneState() } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -796,58 +798,12 @@ class FileListViewController: UIViewController, UICollectionViewDataSource, Swip // MARK: - State restoration - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(viewModel.driveFileManager.drive.id, forKey: "DriveID") - coder.encode(viewModel.currentDirectory.id, forKey: "DirectoryID") - if let viewModel { - coder.encode(String(describing: type(of: viewModel)), forKey: "ViewModel") - } - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveID") - let directoryId = coder.decodeInteger(forKey: "DirectoryID") - let viewModelName = coder.decodeObject(of: NSString.self, forKey: "ViewModel") as String? - - // Drive File Manager should be consistent - let maybeDriveFileManager: DriveFileManager? - #if ISEXTENSION - maybeDriveFileManager = accountManager.getDriveFileManager(for: driveId, userId: accountManager.currentUserId) - #else - if viewModelName == String(describing: SharedWithMeViewModel.self) { - maybeDriveFileManager = accountManager.getDriveFileManager(for: driveId, userId: accountManager.currentUserId) - } else { - maybeDriveFileManager = (tabBarController as? MainTabViewController)?.driveFileManager - } - #endif - guard let driveFileManager = maybeDriveFileManager else { - // Handle error? - return - } - let maybeCurrentDirectory = driveFileManager.getCachedFile(id: directoryId) - - if !(maybeCurrentDirectory == nil && directoryId > DriveFileManager.constants.rootID), - let viewModelName, - let viewModel = getViewModel( - viewModelName: viewModelName, - driveFileManager: driveFileManager, - currentDirectory: maybeCurrentDirectory - ) { - self.viewModel = viewModel - setupViewModel() - tryLoadingFilesOrDisplayError() - } else { - // We need some view model to restore the view controller and pop it... - viewModel = ConcreteFileListViewModel( - driveFileManager: driveFileManager, - currentDirectory: driveFileManager.getCachedRootFile() - ) - navigationController?.popViewController(animated: true) - } + var currentSceneMetadata: [AnyHashable: Any] { + [ + SceneRestorationKeys.lastViewController.rawValue: SceneRestorationScreens.FileListViewController.rawValue, + SceneRestorationValues.driveId.rawValue: driveFileManager.drive.id, + SceneRestorationValues.fileId.rawValue: viewModel.currentDirectory.id + ] } // MARK: - Files header view delegate diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 271984fa4..be9607cb4 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -107,7 +107,7 @@ class FileListViewModel: SelectDelegate { /// Internal realm collection of Files observed /// /// They should be frozen by convention. - #if DEBUG || TEST + #if DEBUG var _frozenFiles = AnyRealmCollection(List()) { willSet { for item in newValue { diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index 05c92a17e..61d2d5340 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -152,7 +152,6 @@ extension FileActionsFloatingPanelViewController { private func informationsAction() { let fileDetailViewController = FileDetailViewController.instantiate(driveFileManager: driveFileManager, file: file) - fileDetailViewController.file = file presentingParent?.navigationController?.pushViewController(fileDetailViewController, animated: true) dismiss(animated: true) } diff --git a/kDrive/UI/Controller/Files/FileDetailViewController.swift b/kDrive/UI/Controller/Files/FileDetailViewController.swift index 1ad1148fd..c265e3bd1 100644 --- a/kDrive/UI/Controller/Files/FileDetailViewController.swift +++ b/kDrive/UI/Controller/Files/FileDetailViewController.swift @@ -22,7 +22,7 @@ import kDriveCore import kDriveResources import UIKit -class FileDetailViewController: UIViewController { +class FileDetailViewController: UIViewController, SceneStateRestorable { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var commentButton: UIButton! @@ -157,6 +157,8 @@ class FileDetailViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) MatomoUtils.track(view: ["FileDetail"]) + + saveSceneState() } override func viewWillDisappear(_ animated: Bool) { @@ -206,20 +208,20 @@ class FileDetailViewController: UIViewController { tableView.separatorColor = .clear - // Set initial rows fileInformationRows = FileInformationRow.getRows(for: file, fileAccess: fileAccess, contentCount: contentCount, categoryRights: driveFileManager.drive.categoryRights) - // Load file informations loadFileInformation() - // Observe file changes driveFileManager.observeFileUpdated(self, fileId: file.id) { newFile in Task { @MainActor [weak self] in - self?.file = newFile - self?.reloadTableView() + guard let self else { + return + } + self.file = newFile + self.reloadTableView() } } } @@ -480,45 +482,12 @@ class FileDetailViewController: UIViewController { // MARK: - State restoration - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - coder.encode(file.id, forKey: "FileId") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - let fileId = coder.decodeInteger(forKey: "FileId") - - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, userId: accountManager.currentUserId) else { - return - } - self.driveFileManager = driveFileManager - file = driveFileManager.getCachedFile(id: fileId) - guard file != nil else { - // If file doesn't exist anymore, pop view controller - navigationController?.popViewController(animated: true) - return - } - Task { [proxyFile = file.proxify(), isDirectory = file.isDirectory] in - async let currentFileAccess = driveFileManager.apiFetcher.access(for: proxyFile) - async let folderContentCount = isDirectory ? driveFileManager.apiFetcher.count(of: proxyFile) : nil - - fileInformationRows = try await FileInformationRow.getRows(for: file, - fileAccess: currentFileAccess, - contentCount: folderContentCount, - categoryRights: driveFileManager.drive - .categoryRights) - fileAccess = try await currentFileAccess - contentCount = try await folderContentCount - - if tableView.window != nil && currentTab == .informations { - reloadTableView() - } - } + var currentSceneMetadata: [AnyHashable: Any] { + [ + SceneRestorationKeys.lastViewController.rawValue: SceneRestorationScreens.FileDetailViewController.rawValue, + SceneRestorationValues.driveId.rawValue: driveFileManager.drive.id, + SceneRestorationValues.fileId.rawValue: file.id + ] } } diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 5b8b64156..f0b8914aa 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -41,21 +41,21 @@ final class FilePresenter { return } - // Pop current navigation stack viewController.navigationController?.popToRootViewController(animated: false) - // Dismiss all view controllers presented + rootViewController.dismiss(animated: false) { - // Select Files tab - rootViewController.selectedIndex = 1 + rootViewController.selectedIndex = MainTabIndex.files.rawValue guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { return } - // Pop to root navigationController.popToRootViewController(animated: false) - // Present file - guard let fileListViewController = navigationController.topViewController as? FileListViewController else { return } + + guard let fileListViewController = navigationController.topViewController as? FileListViewController else { + return + } + let filePresenter = FilePresenter(viewController: fileListViewController) filePresenter.presentParent(of: file, driveFileManager: driveFileManager, animated: false) } @@ -132,13 +132,16 @@ final class FilePresenter { } } - private func presentDirectory( + public func presentDirectory( for file: File, driveFileManager: DriveFileManager, animated: Bool, completion: ((Bool) -> Void)? ) { - // Show files list + defer { + completion?(true) + } + let viewModel: FileListViewModel if driveFileManager.drive.sharedWithMe { viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file) @@ -147,37 +150,40 @@ final class FilePresenter { } 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) - } + guard file.isDisabled else { + navigationController?.pushViewController(nextVC, animated: animated) + return + } + + guard driveFileManager.drive.isUserAdmin else { + viewController?.present(NoAccessFloatingPanelViewController.instantiatePanel(), animated: true) + return + } + + 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) } - } else { - navigationController?.pushViewController(nextVC, animated: animated) } - completion?(true) + + viewController?.present(accessFileDriveFloatingPanelController, animated: true) } private func downloadAndPresentBookmark(for file: File, completion: ((Bool) -> Void)?) { diff --git a/kDrive/UI/Controller/Files/Files.storyboard b/kDrive/UI/Controller/Files/Files.storyboard index c77db9c6f..0cd81c1e4 100644 --- a/kDrive/UI/Controller/Files/Files.storyboard +++ b/kDrive/UI/Controller/Files/Files.storyboard @@ -1,9 +1,9 @@ - + - + @@ -13,7 +13,7 @@ - + @@ -48,7 +48,7 @@ - + @@ -71,7 +71,7 @@ - + @@ -127,7 +127,7 @@ - + @@ -184,7 +184,7 @@ - + @@ -217,7 +217,7 @@ - + @@ -247,7 +247,7 @@ - + @@ -298,7 +298,7 @@ - + @@ -349,7 +349,7 @@ - + @@ -371,7 +371,7 @@ - + @@ -423,7 +423,7 @@ - + @@ -481,7 +481,7 @@ - + @@ -539,7 +539,7 @@ - + @@ -578,7 +578,7 @@ - + @@ -646,7 +646,7 @@ - + diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift index 494d74596..4cefe6aaf 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift @@ -257,7 +257,7 @@ extension MultipleSelectionFloatingPanelViewController { // Present from root view controller if the panel is no longer presented let viewController = self.view.window != nil ? self - : (UIApplication.shared.delegate as! AppDelegate).topMostViewController + : self.appNavigable.topMostViewController guard viewController as? UIDocumentPickerViewController == nil else { return } let documentExportViewController = UIDocumentPickerViewController( url: downloadedArchiveUrl, diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index 7f2678b87..639678c3a 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -25,6 +25,7 @@ import UIKit final class MultipleSelectionFloatingPanelViewController: UICollectionViewController { @LazyInjectService var accountManager: AccountManageable + @LazyInjectService var appNavigable: AppNavigable var driveFileManager: DriveFileManager! var files = [File]() diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 8fcffb114..89db71e23 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -33,7 +33,7 @@ protocol PreviewContentCellDelegate: AnyObject { func openWith(from: UIView) } -class PreviewViewController: UIViewController, PreviewContentCellDelegate { +class PreviewViewController: UIViewController, PreviewContentCellDelegate, SceneStateRestorable { @LazyInjectService var accountManager: AccountManageable class PreviewError { @@ -80,6 +80,7 @@ class PreviewViewController: UIViewController, PreviewContentCellDelegate { private var currentIndex = IndexPath(row: 0, section: 0) { didSet { setTitle() + saveSceneState() } } @@ -260,6 +261,8 @@ class PreviewViewController: UIViewController, PreviewContentCellDelegate { heightToHide = backButton.frame.minY MatomoUtils.track(view: [MatomoUtils.Views.preview.displayName, "File"]) + + saveSceneState() } override func viewWillDisappear(_ animated: Bool) { @@ -580,58 +583,15 @@ class PreviewViewController: UIViewController, PreviewContentCellDelegate { // MARK: - State restoration - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - coder.encode(previewFiles.map(\.id), forKey: "Files") - coder.encode(currentIndex.row, forKey: "CurrentIndex") - coder.encode(initialLoading, forKey: "InitialLoading") - coder.encode(normalFolderHierarchy, forKey: "NormalFolderHierarchy") - coder.encode(fromActivities, forKey: "FromActivities") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - initialLoading = coder.decodeBool(forKey: "InitialLoading") - normalFolderHierarchy = coder.decodeBool(forKey: "NormalFolderHierarchy") - fileInformationsViewController.normalFolderHierarchy = normalFolderHierarchy - fromActivities = coder.decodeBool(forKey: "FromActivities") - if fromActivities { - floatingPanelViewController.surfaceView.grabberHandle.isHidden = true - } - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, - userId: accountManager.currentUserId) else { - navigationController?.popViewController(animated: true) - return - } - self.driveFileManager = driveFileManager - let previewFileIds = coder.decodeObject(forKey: "Files") as? [Int] ?? [] - - let matchedFiles = driveFileManager.database.fetchResults(ofType: File.self) { lazyCollection in - lazyCollection.filter("id IN %@", previewFileIds) - } - - previewFiles = Array(matchedFiles) - - let decodedIndex = coder.decodeInteger(forKey: "CurrentIndex") - if decodedIndex >= previewFiles.count { - navigationController?.popViewController(animated: true) - return - } - currentIndex = IndexPath(row: decodedIndex, section: 0) - - // Update UI - Task { @MainActor [self] in - collectionView.reloadData() - updateFileForCurrentIndex() - collectionView.scrollToItem(at: currentIndex, at: .centeredVertically, animated: false) - updateNavigationBar() - downloadFileIfNeeded(at: currentIndex) - } - observeFileUpdated() + var currentSceneMetadata: [AnyHashable: Any] { + [ + SceneRestorationKeys.lastViewController.rawValue: SceneRestorationScreens.PreviewViewController.rawValue, + SceneRestorationValues.driveId.rawValue: driveFileManager.drive.id, + SceneRestorationValues.Carousel.filesIds.rawValue: previewFiles.map(\.id), + SceneRestorationValues.Carousel.currentIndex.rawValue: currentIndex.row, + SceneRestorationValues.Carousel.normalFolderHierarchy.rawValue: normalFolderHierarchy, + SceneRestorationValues.Carousel.fromActivities.rawValue: fromActivities + ] } } diff --git a/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift b/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift index 399fae997..94cb44443 100644 --- a/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift +++ b/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift @@ -44,25 +44,6 @@ class RecentActivityFilesViewModel: InMemoryFileListViewModel { currentDirectory: DriveFileManager.homeRootFile ) } - - func encodeRestorableState(with coder: NSCoder) { - coder.encode(activity?.id ?? 0, forKey: "ActivityId") - coder.encode(getAllFiles().map(\.id), forKey: "Files") - } - - func decodeRestorableState(with coder: NSCoder) { - let activityId = coder.decodeInteger(forKey: "ActivityId") - let fileIds = coder.decodeObject(forKey: "Files") as? [Int] ?? [] - - // TODO: fixme with proper fetch transaction + pred - try? driveFileManager.database.writeTransaction { realm in - activity = realm.object(ofType: FileActivity.self, forPrimaryKey: activityId)?.freeze() - let cachedFiles = fileIds.compactMap { driveFileManager.getCachedFile(id: $0, using: realm) }.map { $0.detached() } - addPage(files: cachedFiles, fullyDownloaded: true, cursor: nil) - } - - forceRefresh() - } } class RecentActivityFilesViewController: FileListViewController { @@ -139,18 +120,4 @@ class RecentActivityFilesViewController: FileListViewController { } return super.collectionView(collectionView, actionsFor: cell, at: indexPath) } - - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - activityViewModel?.encodeRestorableState(with: coder) - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - activityViewModel?.decodeRestorableState(with: coder) - } } diff --git a/kDrive/UI/Controller/Files/Rights and Share/InviteUserViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/InviteUserViewController.swift index 3f20f6d83..708dc37f4 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/InviteUserViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/InviteUserViewController.swift @@ -190,45 +190,6 @@ class InviteUserViewController: UIViewController { class func instantiate() -> InviteUserViewController { return Storyboard.files.instantiateViewController(withIdentifier: "InviteUserViewController") as! InviteUserViewController } - - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - coder.encode(file.id, forKey: "FileId") - coder.encode(emails, forKey: "Emails") - coder.encode(userIds, forKey: "UserIds") - coder.encode(teamIds, forKey: "TeamIds") - coder.encode(newPermission.rawValue, forKey: "NewPermission") - coder.encode(message, forKey: "Message") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - let fileId = coder.decodeInteger(forKey: "FileId") - emails = coder.decodeObject(forKey: "Emails") as? [String] ?? [] - let restoredUserIds = coder.decodeObject(forKey: "UserIds") as? [Int] ?? [] - let restoredTeamIds = coder.decodeObject(forKey: "TeamIds") as? [Int] ?? [] - newPermission = UserPermission(rawValue: coder.decodeObject(forKey: "NewPermission") as? String ?? "") ?? .read - message = coder.decodeObject(forKey: "Message") as? String ?? "" - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, - userId: accountManager.currentUserId) else { - return - } - self.driveFileManager = driveFileManager - file = driveFileManager.getCachedFile(id: fileId) - - shareables = restoredUserIds.compactMap { driveInfosManager.getUser(primaryKey: $0) } - + restoredTeamIds.compactMap { driveInfosManager.getTeam(primaryKey: $0) } - - // Update UI - setTitle() - reloadInvited() - } } // MARK: - UITableViewDelegate, UITableViewDataSource diff --git a/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift index e2ceee542..32dc54268 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift @@ -143,30 +143,6 @@ class ShareAndRightsViewController: UIViewController { viewController.file = file return viewController } - - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - coder.encode(file.id, forKey: "FileId") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - let fileId = coder.decodeInteger(forKey: "FileId") - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, - userId: accountManager.currentUserId) else { - return - } - self.driveFileManager = driveFileManager - file = driveFileManager.getCachedFile(id: fileId) - setTitle() - updateShareList() - } } // MARK: - Table view delegate & data source diff --git a/kDrive/UI/Controller/Files/Rights and Share/ShareLinkSettingsViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/ShareLinkSettingsViewController.swift index 6ead2e180..ed18c8677 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/ShareLinkSettingsViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/ShareLinkSettingsViewController.swift @@ -220,32 +220,6 @@ class ShareLinkSettingsViewController: UIViewController { return Storyboard.files .instantiateViewController(withIdentifier: "ShareLinkSettingsViewController") as! ShareLinkSettingsViewController } - - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(driveFileManager.drive.id, forKey: "DriveId") - coder.encode(file.id, forKey: "FileId") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveId") - let fileId = coder.decodeInteger(forKey: "FileId") - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, - userId: accountManager.currentUserId) else { - return - } - self.driveFileManager = driveFileManager - file = driveFileManager.getCachedFile(id: fileId) - // Update UI - initOptions() - updateButton() - tableView.reloadData() - } } // MARK: - UITableViewDelegate, UITableViewDataSource diff --git a/kDrive/UI/Controller/Files/RootMenuViewController.swift b/kDrive/UI/Controller/Files/RootMenuViewController.swift index 10f07562d..39343b87f 100644 --- a/kDrive/UI/Controller/Files/RootMenuViewController.swift +++ b/kDrive/UI/Controller/Files/RootMenuViewController.swift @@ -148,6 +148,12 @@ class RootMenuViewController: CustomLargeTitleCollectionViewController, SelectSw (tabBarController as? PlusButtonObserver)?.updateCenterButton() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + saveSceneState() + } + @objc func presentSearch() { let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager) let searchViewController = SearchViewController.instantiateInNavigationController(viewModel: viewModel) @@ -268,4 +274,10 @@ class RootMenuViewController: CustomLargeTitleCollectionViewController, SelectSw let destinationViewController = FileListViewController.instantiate(viewModel: destinationViewModel) navigationController?.pushViewController(destinationViewController, animated: true) } + + // MARK: - State restoration + + var currentSceneMetadata: [AnyHashable: Any] { + [:] + } } diff --git a/kDrive/UI/Controller/Files/Save File/SaveFile.storyboard b/kDrive/UI/Controller/Files/Save File/SaveFile.storyboard index cc9d8a4a4..43d6edcd0 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFile.storyboard +++ b/kDrive/UI/Controller/Files/Save File/SaveFile.storyboard @@ -1,9 +1,9 @@ - + - + @@ -14,7 +14,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -67,7 +67,7 @@ - + @@ -167,7 +167,7 @@ - + @@ -212,7 +212,7 @@ - + diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index c6560d416..e4b3e06b4 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -463,7 +463,7 @@ extension SaveFileViewController: SelectFolderDelegate { extension SaveFileViewController: SelectDriveDelegate { func didSelectDrive(_ drive: Drive) { - if let selectedDriveFileManager = accountManager.getDriveFileManager(for: drive) { + if let selectedDriveFileManager = accountManager.getDriveFileManager(for: drive.id, userId: drive.userId) { self.selectedDriveFileManager = selectedDriveFileManager selectedDirectory = selectedDriveFileManager.getCachedRootFile() sections = [.fileName, .driveSelection, .directorySelection] diff --git a/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift b/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift index 538458f54..c64097a64 100644 --- a/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift @@ -183,19 +183,4 @@ class SelectFolderViewController: FileListViewController { navigationController?.pushViewController(nextVC, animated: true) } } - - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(disabledDirectoriesSelection, forKey: "DisabledDirectories") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - disabledDirectoriesSelection = coder.decodeObject(forKey: "DisabledDirectories") as? [Int] ?? [] - setUpDirectory() - } } diff --git a/kDrive/UI/Controller/Files/Search/Search.storyboard b/kDrive/UI/Controller/Files/Search/Search.storyboard index 19c66dae5..e20d6e549 100644 --- a/kDrive/UI/Controller/Files/Search/Search.storyboard +++ b/kDrive/UI/Controller/Files/Search/Search.storyboard @@ -1,9 +1,9 @@ - + - + @@ -12,7 +12,7 @@ - + @@ -65,9 +65,9 @@ - + - + diff --git a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift index f40bd60b3..855f0b81f 100644 --- a/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift +++ b/kDrive/UI/Controller/Files/Upload/UploadQueueViewController.swift @@ -124,30 +124,6 @@ final class UploadQueueViewController: UIViewController { return Storyboard.files .instantiateViewController(withIdentifier: "UploadQueueViewController") as! UploadQueueViewController } - - // MARK: - State restoration - - override func encodeRestorableState(with coder: NSCoder) { - super.encodeRestorableState(with: coder) - - coder.encode(currentDirectory.driveId, forKey: "DriveID") - coder.encode(currentDirectory.id, forKey: "DirectoryID") - } - - override func decodeRestorableState(with coder: NSCoder) { - super.decodeRestorableState(with: coder) - - let driveId = coder.decodeInteger(forKey: "DriveID") - let directoryId = coder.decodeInteger(forKey: "DirectoryID") - - guard let driveFileManager = accountManager.getDriveFileManager(for: driveId, userId: accountManager.currentUserId), - let directory = driveFileManager.getCachedFile(id: directoryId) else { - // Handle error? - return - } - currentDirectory = directory - setUpObserver() - } } // MARK: - Table view data source diff --git a/kDrive/UI/Controller/Floating Panel Information/InformationFloatingPanel.storyboard b/kDrive/UI/Controller/Floating Panel Information/InformationFloatingPanel.storyboard index 5b36f83df..32797857c 100644 --- a/kDrive/UI/Controller/Floating Panel Information/InformationFloatingPanel.storyboard +++ b/kDrive/UI/Controller/Floating Panel Information/InformationFloatingPanel.storyboard @@ -1,9 +1,9 @@ - + - + @@ -18,7 +18,7 @@ - + @@ -44,7 +44,7 @@ - + @@ -86,7 +86,7 @@ - + - + @@ -42,7 +42,7 @@