diff --git a/Examples/GroceryExpress/AppDelegate.swift b/Examples/GroceryExpress/AppDelegate.swift index c9e1ca2..1f70c0a 100644 --- a/Examples/GroceryExpress/AppDelegate.swift +++ b/Examples/GroceryExpress/AppDelegate.swift @@ -19,11 +19,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private let connectionRedirectHandler = ConnectionRedirectHandler(redirectURL: AppDelegate.connectionRedirectURL) func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - ConnectButtonController.synchronizationLoggingEnabled = true ConnectButtonController.analyticsEnabled = true ConnectButtonController.initialize(options: .init(enableSDKBackgroundProcess: true, showPermissionsPrompts: true)) - ConnectButtonController.activate(connections: [DisplayInformation.locationConnection.connectionId]) + if ConnectionCredentials(settings: .init()).isLoggedIn { + ConnectButtonController.activate(connections: [DisplayInformation.locationConnection.connectionId]) + } else { + ConnectButtonController.deactivate() + } ConnectButtonController.setBackgroundProcessClosures { print("Background process started!") } expirationHandler: { diff --git a/Examples/GroceryExpress/ConnectionCredentials.swift b/Examples/GroceryExpress/ConnectionCredentials.swift index 775b8a7..7b719f9 100644 --- a/Examples/GroceryExpress/ConnectionCredentials.swift +++ b/Examples/GroceryExpress/ConnectionCredentials.swift @@ -58,12 +58,17 @@ class ConnectionCredentials: ConnectionCredentialProvider, CustomStringConvertib Keys.token : token ] UserDefaults.standard.set(user, forKey: Keys.user) + ConnectButtonController.activate( + connections: [DisplayInformation.locationConnection.connectionId], + lifecycleSynchronizationOptions: .all + ) } /// Clears the active IFTTT session func logout() { userToken = nil UserDefaults.standard.set(nil, forKey: Keys.user) + ConnectButtonController.deactivate() } /// Creates an instance of ConnectionCredentials diff --git a/Examples/GroceryExpress/SettingsViewController.swift b/Examples/GroceryExpress/SettingsViewController.swift index 72a7642..9102b07 100644 --- a/Examples/GroceryExpress/SettingsViewController.swift +++ b/Examples/GroceryExpress/SettingsViewController.swift @@ -60,7 +60,6 @@ class SettingsViewController: UIViewController { } @IBAction func logoutTapped(_ sender: Any) { ConnectionCredentials(settings: settings).logout() - ConnectButtonController.deactivate() update() } diff --git a/IFTTT SDK.xcodeproj/project.pbxproj b/IFTTT SDK.xcodeproj/project.pbxproj index 5cb9a82..f4ac5e7 100644 --- a/IFTTT SDK.xcodeproj/project.pbxproj +++ b/IFTTT SDK.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ B556033C25A851EE00A29A01 /* Localizable_cs.strings in Resources */ = {isa = PBXBuildFile; fileRef = B556032425A851EE00A29A01 /* Localizable_cs.strings */; }; B556033D25A851EE00A29A01 /* Localizable_nl.strings in Resources */ = {isa = PBXBuildFile; fileRef = B556032525A851EE00A29A01 /* Localizable_nl.strings */; }; DE25265623D8C49D0019C9CB /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE25265523D8C49D0019C9CB /* Analytics.swift */; }; + DE260F6B26CAFC20004191D1 /* SynchronizationSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE260F6A26CAFC20004191D1 /* SynchronizationSchedulerTests.swift */; }; DE2906D3242BF66E00CC2825 /* Connection+Parsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2906D2242BF66E00CC2825 /* Connection+Parsing.swift */; }; DE2F524A2429404200EF986A /* Connection+Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2F52492429404200EF986A /* Connection+Location.swift */; }; DE2F524C242940AD00EF986A /* CLCircularRegion+Parsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2F524B242940AD00EF986A /* CLCircularRegion+Parsing.swift */; }; @@ -226,6 +227,7 @@ B556032525A851EE00A29A01 /* Localizable_nl.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = Localizable_nl.strings; path = Resources/Localizable_nl.strings; sourceTree = ""; }; DE1712BC2565B295000B13E6 /* RegionEventsRegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionEventsRegistryTests.swift; sourceTree = ""; }; DE25265523D8C49D0019C9CB /* Analytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; + DE260F6A26CAFC20004191D1 /* SynchronizationSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizationSchedulerTests.swift; sourceTree = ""; }; DE2906D2242BF66E00CC2825 /* Connection+Parsing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+Parsing.swift"; sourceTree = ""; }; DE2F52492429404200EF986A /* Connection+Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Connection+Location.swift"; sourceTree = ""; }; DE2F524B242940AD00EF986A /* CLCircularRegion+Parsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLCircularRegion+Parsing.swift"; sourceTree = ""; }; @@ -439,6 +441,7 @@ DEC29DE525840DE300BF56EE /* LocationServiceTests.swift */, DEC29EE7258419FC00BF56EE /* Info.plist */, DEF4A4962587BA1A00735E98 /* ArrayHelpersTests.swift */, + DE260F6A26CAFC20004191D1 /* SynchronizationSchedulerTests.swift */, ); path = SDKHostAppTests; sourceTree = ""; @@ -864,6 +867,7 @@ DEC29F0125841A0800BF56EE /* String_EmailDataDetectorTests.swift in Sources */, DEC29F0525841A0800BF56EE /* RegionsMonitorTests.swift in Sources */, DEC29F0625841A0800BF56EE /* Connection_ParsingTests.swift in Sources */, + DE260F6B26CAFC20004191D1 /* SynchronizationSchedulerTests.swift in Sources */, DEF4A4972587BA1A00735E98 /* ArrayHelpersTests.swift in Sources */, DEC29F0225841A0800BF56EE /* EventPublisherTests.swift in Sources */, ); diff --git a/IFTTT SDK/ConnectButtonController+Public.swift b/IFTTT SDK/ConnectButtonController+Public.swift index f6b8ce3..7a11230 100644 --- a/IFTTT SDK/ConnectButtonController+Public.swift +++ b/IFTTT SDK/ConnectButtonController+Public.swift @@ -70,25 +70,24 @@ extension ConnectButtonController { } } - /// Performs setup of the SDK. Starts the synchronization of in the SDK. Registers background process with the system if desired. + /// Performs setup of the SDK.Activated synchronization /// /// - Parameters: /// - credentials: An optional object conforming to `ConnectionCredentialProvider` which is used to setup the SDK. If this is nil, the SDK will attempt to use cached values. - /// - lifecycleSynchronizationOptions: An instance of `ApplicationLifecycleSynchronizationOptions` that defines which app lifecycle events the synchronization should occur on. If this parameter is not set, a default value of `ApplicationLifecycleSynchronizationOptions.all` will be used. - public static func setup(with credentials: ConnectionCredentialProvider?, - lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions = .all) { + public static func setup(with credentials: ConnectionCredentialProvider?) { if let credentials = credentials { Keychain.update(with: credentials) } - ConnectionsSynchronizer.shared.setup(lifecycleSynchronizationOptions: lifecycleSynchronizationOptions) } /// Call this method to activate the synchronization. This starts synchronization for the parameter connections. /// /// - Parameters: /// - connections: An optional list of `Connection` to activate synchronization with. - public static func activate(connections ids: [String]? = nil) { - ConnectionsSynchronizer.shared.activate(connections: ids) + /// - lifecycleSynchronizationOptions: An instance of `ApplicationLifecycleSynchronizationOptions` that defines which app lifecycle events the synchronization should occur on. If this parameter is not set, a default value of `ApplicationLifecycleSynchronizationOptions.all` will be used. + public static func activate(connections ids: [String]? = nil, + lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions = .all) { + ConnectionsSynchronizer.shared.activate(connections: ids, lifecycleSynchronizationOptions: lifecycleSynchronizationOptions) } /// Call this method to deactivate the synchronization of connection and native service data. This stops synchronization and performs cleanup of any stored data. This will also remove any registered geofences. diff --git a/IFTTT SDK/ConnectionsSynchronizer.swift b/IFTTT SDK/ConnectionsSynchronizer.swift index 764443e..ab35570 100644 --- a/IFTTT SDK/ConnectionsSynchronizer.swift +++ b/IFTTT SDK/ConnectionsSynchronizer.swift @@ -133,14 +133,6 @@ final class ConnectionsSynchronizer { location.start() } - /// Performs basic setup of the SDK with Application lifecycle synchronization options. - /// - /// - Parameters: - /// - lifecycleSynchronizationOptions: The synchronization options to use in setting up App Lifecycle notification observers. - func setup(lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions) { - scheduler.setup(lifecycleSynchronizationOptions: lifecycleSynchronizationOptions) - } - /// Can be used to force a synchronization. /// /// - Parameters: @@ -151,19 +143,20 @@ final class ConnectionsSynchronizer { eventPublisher.onNext(event) } - /// Used to start the synchronization with an optional list of connection ids to monitor. + /// Used to start the synchronization. /// /// - Parameters: /// - connections: An optional list of connections to start monitoring. - func activate(connections ids: [String]? = nil) { + /// - lifecycleSynchronizationOptions: The app lifecycle synchronization options to use with the scheduler + func activate(connections ids: [String]? = nil, lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions) { + start(lifecycleSynchronizationOptions: lifecycleSynchronizationOptions) + update(isActivation: true) if let ids = ids { registry.addConnections(with: ids, shouldNotify: false) ConnectButtonController.synchronizationLog("Activated synchronization with connection ids: \(ids)") } else { ConnectButtonController.synchronizationLog("Activated synchronization") } - start() - update(isActivation: true) } /// Used to deactivate and stop synchronization. @@ -174,19 +167,17 @@ final class ConnectionsSynchronizer { } /// Call this to start the synchronization. Safe to be called multiple times. - private func start() { + private func start(lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions) { if state == .running { return } - setupNotifications() performPreflightChecks() Keychain.resetIfNecessary(force: false) - scheduler.start() + scheduler.start(lifecycleSynchronizationOptions: lifecycleSynchronizationOptions) state = .running } /// Call this to stop the synchronization completely. Safe to be called multiple times. private func stop() { - stopNotifications() Keychain.resetIfNecessary(force: true) scheduler.stop() state = .stopped @@ -198,22 +189,7 @@ final class ConnectionsSynchronizer { ConnectButtonController.synchronizationLog("Background location not enabled for this target! Enable background location to allow location updates to be delivered to the app in the background.") } } - - /// Peforms internal setup to allow the SDK to perform work in response to notification center notifications. - private func setupNotifications() { - if #available(iOS 13.0, *) { - NotificationCenter.default.addObserver(self, - selector: #selector(applicationDidEnterBackground), - name: UIApplication.didEnterBackgroundNotification, - object: nil) - } - } - - /// Stops notification observation - private func stopNotifications() { - NotificationCenter.default.removeObserver(self) - } - + private func setupRegistryNotifications() { NotificationCenter.default.addObserver(forName: .UpdateConnectionsName, object: nil, diff --git a/IFTTT SDK/SynchronizationManager.swift b/IFTTT SDK/SynchronizationManager.swift index e8dde76..1c7c278 100644 --- a/IFTTT SDK/SynchronizationManager.swift +++ b/IFTTT SDK/SynchronizationManager.swift @@ -36,7 +36,7 @@ final class SynchronizationManager { /// Creates a `SyncManager` /// /// - Parameter subscribers: The `SyncSubscribers to perform during syncs - init(subscribers: Array) { + init(subscribers: [SynchronizationSubscriber]) { self.subscribers = subscribers } @@ -126,7 +126,7 @@ extension SynchronizationManager { finish() } - private(set) var subscribers: Array = [] + private(set) var subscribers = [SynchronizationSubscriber]() private var resultsBySubscriber: [String : UIBackgroundFetchResult] = [:] diff --git a/IFTTT SDK/SynchronizationScheduler.swift b/IFTTT SDK/SynchronizationScheduler.swift index 133009e..577cef3 100644 --- a/IFTTT SDK/SynchronizationScheduler.swift +++ b/IFTTT SDK/SynchronizationScheduler.swift @@ -18,16 +18,16 @@ final class SynchronizationScheduler { private let manager: SynchronizationManager /// The token corresponding to the subscriber. - private var subscriberToken: UUID? + private(set) var subscriberToken: UUID? /// The tokens that get generated when the SDK sets up application lifecycle NotificationCenter observers - private var applicationLifecycleNotificationCenterTokens: [Any] = [] + private(set) var applicationLifecycleNotificationCenterTokens: [Any] = [] /// The tokens that get generated when the SDK sets up other NotificationCenter observers - private var sdkGeneratedNotificationCenterTokens: [Any] = [] + private(set) var sdkGeneratedNotificationCenterTokens: [Any] = [] /// Has the app opted into using background process? - private var optedInToUsingSDKBackgroundProcess: Bool = false + private(set) var optedInToUsingSDKBackgroundProcess: Bool = false /// The triggers to kick off synchronizations private let triggers: EventPublisher @@ -49,53 +49,29 @@ final class SynchronizationScheduler { /// - Parameters: /// - syncManager: The `SyncManager` to use when scheduling a sync /// - triggers: The publisher to use in publishing any possible events we might need to. - init(manager: SynchronizationManager, - triggers: EventPublisher) { + init(manager: SynchronizationManager, triggers: EventPublisher) { self.manager = manager self.triggers = triggers - setupSubscribers() - } - - func setup(lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions) { - removeLifecycleNotificationObservers() - - // Start synchronization on system events - var appLifecycleEventTuples = [(NSNotification.Name, SynchronizationSource, Bool)]() - - if lifecycleSynchronizationOptions.contains(.applicationDidBecomeActive) { - appLifecycleEventTuples.append((UIApplication.didBecomeActiveNotification, .appDidBecomeActive, false)) - } - if lifecycleSynchronizationOptions.contains(.applicationDidEnterBackground) { - appLifecycleEventTuples.append((UIApplication.didEnterBackgroundNotification, .appBackgrounded, true)) - } - - self.applicationLifecycleNotificationCenterTokens = appLifecycleEventTuples.map { - return scheduleSynchronization(on: $0.0, - source: $0.1, - shouldRunInBackground: $0.2) - } + startAppSubscribers() } /// Performs registration for system and SDK generated events for kicking off synchronizations /// Should get called when the scheduler is to start. - func start() { - // Remove any previous tokens that might still be aroind + func start(lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions) { + // Remove any previous tokens that might still be around removeSDKGeneratedNotificationObservers() + removeLifecycleNotificationObservers() + removeAppSubscribers() // Start the manager manager.start() - // Start monitoring the notifications related to Connection CRUD operations - let eventTuples: [(NSNotification.Name, SynchronizationSource, Bool)] = [ - (.ConnectionUpdatedNotification, .connectionsUpdate, true), - (.ConnectionAddedNotification, .connectionAddition, true) - ] - - self.sdkGeneratedNotificationCenterTokens = eventTuples.map { - return scheduleSynchronization(on: $0.0, - source: $0.1, - shouldRunInBackground: $0.2) - } + // Start SDK generated subscribers + startSDKGeneratedSubscribers() + // Start app lifecycle subscribers + startApplicationLifecycleSubscribers(lifecycleSynchronizationOptions: lifecycleSynchronizationOptions) + // Start app generated subscribers() + startAppSubscribers() } /// Unregisters from system and SDK generated events for synchronizations. @@ -104,39 +80,72 @@ final class SynchronizationScheduler { // Reset the manager manager.reset() - // Unregister from background process // Remove observers from notification center removeLifecycleNotificationObservers() removeSDKGeneratedNotificationObservers() - - applicationLifecycleNotificationCenterTokens = [] - sdkGeneratedNotificationCenterTokens = [] + removeAppSubscribers() // Cancel background tasks associated with the SDK cancelBackgroundProcess() - - if let subscriberToken = subscriberToken { - triggers.removeSubscriber(subscriberToken) - } - - // Nil out the subscriber token - subscriberToken = nil } private func removeLifecycleNotificationObservers() { applicationLifecycleNotificationCenterTokens.forEach { NotificationCenter.default.removeObserver($0) } + applicationLifecycleNotificationCenterTokens = [] } private func removeSDKGeneratedNotificationObservers() { sdkGeneratedNotificationCenterTokens.forEach { NotificationCenter.default.removeObserver($0) } + sdkGeneratedNotificationCenterTokens = [] + } + + private func removeAppSubscribers() { + if let subscriberToken = subscriberToken { + triggers.removeSubscriber(subscriberToken) + } + + // Nil out the subscriber token + subscriberToken = nil } - /// Sets up subscriber with app generated triggers. - private func setupSubscribers() { + /// Sets up subscribers with app lifecycle events + private func startApplicationLifecycleSubscribers(lifecycleSynchronizationOptions: ApplicationLifecycleSynchronizationOptions) { + // Start synchronization on system events + var appLifecycleEventTuples = [(NSNotification.Name, SynchronizationSource, Bool)]() + + if lifecycleSynchronizationOptions.contains(.applicationDidBecomeActive) { + appLifecycleEventTuples.append((UIApplication.didBecomeActiveNotification, .appDidBecomeActive, false)) + } + if lifecycleSynchronizationOptions.contains(.applicationDidEnterBackground) { + appLifecycleEventTuples.append((UIApplication.didEnterBackgroundNotification, .appBackgrounded, true)) + } + + var tokens = appLifecycleEventTuples.map { + return scheduleSynchronization(on: $0.0, + source: $0.1, + shouldRunInBackground: $0.2) + } + + if #available(iOS 13.0, *) { + let token = NotificationCenter.default.addObserver( + forName: UIApplication.didEnterBackgroundNotification, + object: nil, + queue: nil) + { [weak self] _ in + self?.applicationDidEnterBackground() + } + tokens.append(token) + } + + self.applicationLifecycleNotificationCenterTokens = tokens + } + + /// Starts subscribers with app generated triggers. + private func startAppSubscribers() { guard subscriberToken == nil else { return } self.subscriberToken = triggers.addSubscriber { [weak self] (triggerEvent) in @@ -150,6 +159,21 @@ final class SynchronizationScheduler { } } + /// Starts subscribers with SDK generated triggers. + private func startSDKGeneratedSubscribers() { + // Start monitoring the notifications related to Connection CRUD operations + let eventTuples: [(NSNotification.Name, SynchronizationSource, Bool)] = [ + (.ConnectionUpdatedNotification, .connectionsUpdate, true), + (.ConnectionAddedNotification, .connectionAddition, true) + ] + + self.sdkGeneratedNotificationCenterTokens = eventTuples.map { + return scheduleSynchronization(on: $0.0, + source: $0.1, + shouldRunInBackground: $0.2) + } + } + /// Helper method to schedule a synchronization with a `NSNotification`. /// /// - Parameters: @@ -215,7 +239,7 @@ final class SynchronizationScheduler { } /// Hook that should get called when the app enters the background. Schedules background process with the system. - func applicationDidEnterBackground() { + @objc func applicationDidEnterBackground() { if #available(iOS 13.0, *) { guard Bundle.main.backgroundProcessingEnabled else { return } guard Bundle.main.containsIFTTTBackgroundProcessingIdentifier else { return } diff --git a/SDKHostAppTests/SynchronizationSchedulerTests.swift b/SDKHostAppTests/SynchronizationSchedulerTests.swift new file mode 100644 index 0000000..116198d --- /dev/null +++ b/SDKHostAppTests/SynchronizationSchedulerTests.swift @@ -0,0 +1,44 @@ +// +// SynchronizationSchedulerTests.swift +// SDKHostAppTests +// +// Copyright © 2021 IFTTT. All rights reserved. +// + +import Foundation +import XCTest + +@testable import IFTTTConnectSDK + +class SynchronizationSchedulerTests: XCTestCase { + private var syncScheduler: SynchronizationScheduler! + private var eventPublisher: EventPublisher! + + override func setUp() { + eventPublisher = .init() + syncScheduler = .init(manager: .init(subscribers: []), triggers: eventPublisher) + } + + override func tearDown() { + eventPublisher = nil + syncScheduler = nil + } + + func test_start() { + syncScheduler.stop() + syncScheduler.start(lifecycleSynchronizationOptions: .all) + + XCTAssertNotNil(syncScheduler.subscriberToken) + XCTAssertTrue(!syncScheduler.applicationLifecycleNotificationCenterTokens.isEmpty) + XCTAssertTrue(!syncScheduler.sdkGeneratedNotificationCenterTokens.isEmpty) + } + + func test_stop() { + syncScheduler.start(lifecycleSynchronizationOptions: .all) + syncScheduler.stop() + + XCTAssertNil(syncScheduler.subscriberToken) + XCTAssertTrue(syncScheduler.applicationLifecycleNotificationCenterTokens.isEmpty) + XCTAssertTrue(syncScheduler.sdkGeneratedNotificationCenterTokens.isEmpty) + } +}