From 05a26c752ac8cad8b4f61704c70212f72e0a8f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 23 May 2024 15:03:47 +0200 Subject: [PATCH] feat: Enabling UIScene delegate --- kDrive/AppDelegate+Launch.swift | 202 +++---- kDrive/AppDelegate+Scene.swift | 50 ++ kDrive/AppDelegate+StateRestoration.swift | 43 ++ kDrive/AppDelegate.swift | 227 +++---- kDrive/Resources/Info.plist | 21 + kDrive/SceneDelegate.swift | 555 ++++++++++++++++++ .../Controller/LockedAppViewController.swift | 3 +- .../Controller/Menu/MenuViewController.swift | 6 +- .../Menu/ParameterTableViewController.swift | 6 +- .../Controller/PreloadingViewController.swift | 9 +- 10 files changed, 874 insertions(+), 248 deletions(-) create mode 100644 kDrive/AppDelegate+Scene.swift create mode 100644 kDrive/AppDelegate+StateRestoration.swift create mode 100644 kDrive/SceneDelegate.swift diff --git a/kDrive/AppDelegate+Launch.swift b/kDrive/AppDelegate+Launch.swift index 121a178e6..651a65042 100644 --- a/kDrive/AppDelegate+Launch.swift +++ b/kDrive/AppDelegate+Launch.swift @@ -28,109 +28,111 @@ 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) - } +// 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() - } + /* + 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 diff --git a/kDrive/AppDelegate+Scene.swift b/kDrive/AppDelegate+Scene.swift new file mode 100644 index 000000000..68af92367 --- /dev/null +++ b/kDrive/AppDelegate+Scene.swift @@ -0,0 +1,50 @@ +/* + 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 UIKit + +extension AppDelegate { + /** iOS 13 or later + UIKit uses this delegate when it is about to create and vend a new UIScene instance to the application. + Use this function to select a configuration to create the new scene with. + You can define the scene configuration in code here, or define it in the Info.plist. + + The application delegate may modify the provided UISceneConfiguration within this function. + If the UISceneConfiguration instance that returns from this function does not have a systemType + that matches the connectingSession's, UIKit asserts. + */ + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + print("•• application configurationForConnecting:\(connectingSceneSession)") + return connectingSceneSession.configuration + } + + /** iOS 13 or later + The system calls this delegate when it removes one or more representations from the -[UIApplication openSessions] set + due to a user interaction or a request from the app itself. If the system discards sessions while the app isn't running, + it calls this function shortly after the app’s next launch. + + Use this function to: + Release any resources that were specific to the discarded scenes, as they will NOT return. + Remove any state or data associated with this session, as it will not return (such as, unsaved draft of a document). + */ + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + print("•• application didDiscardSceneSessions:\(sceneSessions)") + } +} diff --git a/kDrive/AppDelegate+StateRestoration.swift b/kDrive/AppDelegate+StateRestoration.swift new file mode 100644 index 000000000..ee1e6ee42 --- /dev/null +++ b/kDrive/AppDelegate+StateRestoration.swift @@ -0,0 +1,43 @@ +/* + 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 UIKit + +extension AppDelegate { + // For non-scene-based versions of this app on iOS 13.1 and earlier. + func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { + return appRestorationService.shouldSaveApplicationState(coder: coder) + } + + // For non-scene-based versions of this app on iOS 13.1 and earlier. + func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { + return appRestorationService.shouldRestoreApplicationState(coder: coder) + } + + @available(iOS 13.2, *) + // For non-scene-based versions of this app on iOS 13.2 and later. + func application(_ application: UIApplication, shouldSaveSecureApplicationState coder: NSCoder) -> Bool { + return appRestorationService.shouldSaveApplicationState(coder: coder) + } + + @available(iOS 13.2, *) + // For non-scene-based versions of this app on iOS 13.2 and later. + func application(_ application: UIApplication, shouldRestoreSecureApplicationState coder: NSCoder) -> Bool { + return appRestorationService.shouldRestoreApplicationState(coder: coder) + } +} diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 19c8a1b11..d70117a09 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -83,12 +83,15 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg notificationHelper.registerCategories() UNUserNotificationCenter.current().delegate = self - window = UIWindow() + // TODO: Cleanup + // no longer setting up window here +// window = UIWindow() setGlobalTint() let state = UIApplication.shared.applicationState if state != .background { - appWillBePresentedToTheUser() + // TODO: Fixme +// appWillBePresentedToTheUser() } accountManager.delegate = self @@ -173,15 +176,8 @@ 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() - } - } + // Migrated to sceneDidEnterBackground + // func applicationDidEnterBackground(_ application: UIApplication) { } func application( _ application: UIApplication, @@ -197,35 +193,34 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg 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() + // Migrated to sceneWillEnterForeground + // func applicationWillEnterForeground(_ application: UIApplication) { } - Task { - if try await VersionChecker.standard.checkAppVersionStatus() == .updateIsRequired { - prepareRootViewController(currentState: .updateRequired) - } - } - } + // TODO: Remove +// 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() { @@ -237,97 +232,57 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg } } - 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) + // Migrated to sceneDidBecomeActive + // func applicationDidBecomeActive(_ application: UIApplication) { } - 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) - } - } - } +// 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 @@ -354,6 +309,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg func setRootViewController(_ vc: UIViewController, animated: Bool = true) { + fatalError("Woops") guard let window else { return } @@ -416,7 +372,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg @objc func reloadDrive(_ notification: Notification) { Task { @MainActor in - self.refreshCacheScanLibraryAndUpload(preload: false, isSwitching: false) + // TODO: Fixme + // self.refreshCacheScanLibraryAndUpload(preload: false, isSwitching: false) } } @@ -428,16 +385,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg } } - // 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, diff --git a/kDrive/Resources/Info.plist b/kDrive/Resources/Info.plist index 1de821357..f2b143a75 100644 --- a/kDrive/Resources/Info.plist +++ b/kDrive/Resources/Info.plist @@ -91,6 +91,27 @@ To be able to record movies you must allow the use of the microphone NSPhotoLibraryAddUsageDescription Save picture + 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..900b7b9b5 --- /dev/null +++ b/kDrive/SceneDelegate.swift @@ -0,0 +1,555 @@ +/* + 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 + +@available(iOS 13.0, *) +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { + @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 accountManager: AccountManageable + @LazyInjectService var driveInfosManager: DriveInfosManager + @LazyInjectService var keychainHelper: KeychainHelper + @LazyInjectService var backgroundTasksService: BackgroundTasksServiceable + @LazyInjectService var reviewManager: ReviewManageable + @LazyInjectService var availableOfflineManager: AvailableOfflineManageable +// @LazyInjectService var appRestorationService: AppRestorationService + + // TODO: Fixme + private var shortcutItemToProcess: UIApplicationShortcutItem? + + var window: UIWindow? + + /** Apps configure their UIWindow and attach it to the provided UIWindowScene scene. + The system calls willConnectTo shortly after the app delegate's "configurationForConnecting" function. + Use this function to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + + When using a storyboard file, as specified by the Info.plist key, UISceneStoryboardFile, the system automatically configures + the window property and attaches it to the windowScene. + + Remember to retain the SceneDelegate's UIWindow. + The recommended approach is for the SceneDelegate to retain the scene's window. + */ + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + print(" scene session options") + /// 1. Capture the scene + guard let windowScene = (scene as? UIWindowScene) else { return } + + /// 2. Create a new UIWindow using the windowScene constructor which takes in a window scene. + let window = UIWindow(windowScene: windowScene) + + /// 3. Create a view hierarchy programmatically +// let viewController = ArticleListViewController() +// let navigation = UINavigationController(rootViewController: viewController) + + /// 4. Set the root view controller of the window with your view controller +// window.rootViewController = navigation + + /// 5. Set the window and call makeKeyAndVisible() + self.window = window + window.makeKeyAndVisible() + } + + func configure(window: UIWindow?, session: UISceneSession, with activity: NSUserActivity) -> Bool { + print(" configure session with") + return true + } + + /** Use this delegate as the system is releasing the scene or on window close. + This occurs shortly after the scene enters the background, or when the system discards its session. + Release any scene-related resources that the system can recreate the next time the scene connects. + The scene may reconnect later because the system didn't necessarily discard its session (see`application:didDiscardSceneSessions` instead), + so don't delete any user data or state permanently. + */ + func sceneDidDisconnect(_ scene: UIScene) { + print(" sceneDidDisconnect \(scene)") + } + + /** Use this delegate when the scene moves from an active state to an inactive state, on window close, or in iOS enter background. + This may occur due to temporary interruptions (for example, an incoming phone call). + */ + func sceneWillResignActive(_ scene: UIScene) { + print(" sceneWillResignActive \(scene)") + } + + /** Use this delegate as the scene transitions from the background to the foreground, on window open, or in iOS resume. + Use it to undo the changes made on entering the background. + */ + func sceneWillEnterForeground(_ scene: UIScene) { + print(" sceneWillEnterForeground \(scene) \(window)") + @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) + } + } + } + + /** Use this delegate when the scene "has moved" from an inactive state to an active state. + Also use it to restart any tasks that the system paused (or didn't start) when the scene was inactive. + The system calls this delegate every time a scene becomes active so set up your scene UI here. + */ + func sceneDidBecomeActive(_ scene: UIScene) { + print(" sceneDidBecomeActive \(scene)") + 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 + } + } + + /** Use this delegate as the scene transitions from the foreground to the background. + Also use it to save data, release shared resources, and store enough scene-specific state information + to restore the scene to its current state. + */ + func sceneDidEnterBackground(_ scene: UIScene) { + print(" sceneDidEnterBackground \(scene)") + backgroundTasksService.scheduleBackgroundRefresh() + + if UserDefaults.shared.isAppLockEnabled, + !(window?.rootViewController?.isKind(of: LockedAppViewController.self) ?? false) { + lockHelper.setTime() + } + } + + // MARK: - Window Scene + + // Listen for size change. + func windowScene(_ windowScene: UIWindowScene, + didUpdate previousCoordinateSpace: UICoordinateSpace, + interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, + traitCollection previousTraitCollection: UITraitCollection) { + print(" windowScene didUpdate") + } + + // MARK: - Handoff support + + func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) { + print(" scene willContinueUserActivityWithType") + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + print(" scene continue userActivity") + } + + func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) { + print(" scene didFailToContinueUserActivityWithType") + } +} + +// TODO: Refactor with router like pattern and split code away from this class +extension SceneDelegate { + 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) + } + } + } + + func prepareRootViewController(currentState: RootViewControllerState) { + print(" prepareRootViewController:\(currentState)") + 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 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) + } + } + } + + // MARK: Actions + + 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) + } + + /// 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) + } + } + + // MARK: Photo library + + 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() + } + } + + // MARK: Show + + 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 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() + } +} diff --git a/kDrive/UI/Controller/LockedAppViewController.swift b/kDrive/UI/Controller/LockedAppViewController.swift index e6e70144d..03ed253e3 100644 --- a/kDrive/UI/Controller/LockedAppViewController.swift +++ b/kDrive/UI/Controller/LockedAppViewController.swift @@ -41,7 +41,8 @@ class LockedAppViewController: UIViewController { func unlockApp() { appLockHelper.setTime() - (UIApplication.shared.delegate as? AppDelegate)?.updateRootViewControllerState() + // TODO: Fixme +// (UIApplication.shared.delegate as? AppDelegate)?.updateRootViewControllerState() } @IBAction func unlockAppButtonClicked(_ sender: UIButton) { diff --git a/kDrive/UI/Controller/Menu/MenuViewController.swift b/kDrive/UI/Controller/Menu/MenuViewController.swift index cdddae284..b32aa8094 100644 --- a/kDrive/UI/Controller/Menu/MenuViewController.swift +++ b/kDrive/UI/Controller/Menu/MenuViewController.swift @@ -233,12 +233,14 @@ extension MenuViewController { if let nextAccount = self.accountManager.accounts.first { self.accountManager.switchAccount(newAccount: nextAccount) - appDelegate?.refreshCacheScanLibraryAndUpload(preload: true, isSwitching: true) + // TODO: FIXME +// appDelegate?.refreshCacheScanLibraryAndUpload(preload: true, isSwitching: true) } else { SentrySDK.setUser(nil) } self.accountManager.saveAccounts() - appDelegate?.updateRootViewControllerState() + // TODO: Fixme +// appDelegate?.updateRootViewControllerState() } present(alert, animated: true) case .help: diff --git a/kDrive/UI/Controller/Menu/ParameterTableViewController.swift b/kDrive/UI/Controller/Menu/ParameterTableViewController.swift index f4563b869..de2173c12 100644 --- a/kDrive/UI/Controller/Menu/ParameterTableViewController.swift +++ b/kDrive/UI/Controller/Menu/ParameterTableViewController.swift @@ -211,12 +211,14 @@ extension ParameterTableViewController: DeleteAccountDelegate { if let nextAccount = accountManager.accounts.first { accountManager.switchAccount(newAccount: nextAccount) - appDelegate?.refreshCacheScanLibraryAndUpload(preload: true, isSwitching: true) + // TODO: Fixme +// appDelegate?.refreshCacheScanLibraryAndUpload(preload: true, isSwitching: true) } else { SentrySDK.setUser(nil) } accountManager.saveAccounts() - appDelegate?.updateRootViewControllerState() + // TODO: Fixme +// appDelegate?.updateRootViewControllerState() UIConstants.showSnackBar(message: KDriveResourcesStrings.Localizable.snackBarAccountDeleted) } diff --git a/kDrive/UI/Controller/PreloadingViewController.swift b/kDrive/UI/Controller/PreloadingViewController.swift index 83feea125..f6a9ce1ee 100644 --- a/kDrive/UI/Controller/PreloadingViewController.swift +++ b/kDrive/UI/Controller/PreloadingViewController.swift @@ -96,9 +96,11 @@ class PreloadingViewController: UIViewController { _ = try accountManager.getFirstAvailableDriveFileManager(for: currentAccount.userId) if let currentDriveFileManager = accountManager.currentDriveFileManager { - appDelegate.prepareRootViewController(currentState: .mainViewController(currentDriveFileManager)) + // TODO: Fixme +// appDelegate.prepareRootViewController(currentState: .mainViewController(currentDriveFileManager)) } else { - appDelegate.prepareRootViewController(currentState: .onboarding) + // TODO: Fixme +// appDelegate.prepareRootViewController(currentState: .onboarding) } } catch DriveError.NoDriveError.noDrive { let driveErrorViewController = DriveErrorViewController.instantiate(errorType: .noDrive, drive: nil) @@ -111,7 +113,8 @@ class PreloadingViewController: UIViewController { driveErrorNavigationViewController.modalPresentationStyle = .fullScreen present(driveErrorNavigationViewController, animated: true) } catch { - appDelegate.prepareRootViewController(currentState: .onboarding) + // TODO: Fixme +// appDelegate.prepareRootViewController(currentState: .onboarding) } } }