amplify-swift/AmplifyPlugins/Analytics/Sources/AWSPinpointAnalyticsPlugin/Dependency/Pinpoint/Session/ActivityTracking/ActivityTracker.swift

179 lines
5.8 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Amplify
import Foundation
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
enum ActivityEvent {
case applicationDidMoveToBackground
case applicationWillMoveToForeground
case applicationWillTerminate
case backgroundTrackingDidTimeout
}
enum ApplicationState {
case initializing
case runningInForeground
case runningInBackground(isStale: Bool)
case terminated
struct Resolver {
static func resolve(currentState: ApplicationState, event: ActivityEvent) -> ApplicationState {
if case .terminated = currentState {
log.warn("Unexpected state transition. Received event \(event) in \(currentState) state.")
return currentState
}
switch event {
case .applicationWillTerminate:
return .terminated
case .applicationDidMoveToBackground:
return .runningInBackground(isStale: false)
case .applicationWillMoveToForeground:
return .runningInForeground
case .backgroundTrackingDidTimeout:
return .runningInBackground(isStale: true)
}
}
}
}
extension ApplicationState: Equatable {}
extension ApplicationState: DefaultLogger {}
protocol ActivityTrackerBehaviour {
func beginActivityTracking(_ listener: @escaping (ApplicationState) -> Void)
}
class ActivityTracker: ActivityTrackerBehaviour {
#if canImport(UIKit)
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
#endif
private var backgroundTimer: Timer? {
willSet {
backgroundTimer?.invalidate()
}
}
private let backgroundTrackingTimeout: TimeInterval
private let stateMachine: StateMachine<ApplicationState, ActivityEvent>
private var stateMachineSubscriberToken: StateMachineSubscriberToken?
private static let applicationDidMoveToBackgroundNotification: Notification.Name = {
#if canImport(UIKit)
UIApplication.didEnterBackgroundNotification
#else
NSApplication.didResignActiveNotification
#endif
}()
private static let applicationWillMoveToForegoundNotification: Notification.Name = {
#if canImport(UIKit)
UIApplication.willEnterForegroundNotification
#else
NSApplication.willBecomeActiveNotification
#endif
}()
private static var applicationWillTerminateNotification: Notification.Name = {
#if canImport(UIKit)
UIApplication.willTerminateNotification
#else
NSApplication.willTerminateNotification
#endif
}()
private let notifications = [
applicationDidMoveToBackgroundNotification,
applicationWillMoveToForegoundNotification,
applicationWillTerminateNotification
]
init(backgroundTrackingTimeout: TimeInterval,
stateMachine: StateMachine<ApplicationState, ActivityEvent>? = nil) {
self.backgroundTrackingTimeout = backgroundTrackingTimeout
self.stateMachine = stateMachine ?? StateMachine(initialState: .initializing,
resolver: ApplicationState.Resolver.resolve(currentState:event:))
for notification in notifications {
NotificationCenter.default.addObserver(self,
selector: #selector(handleApplicationStateChange),
name: notification,
object: nil)
}
}
deinit {
for notification in notifications {
NotificationCenter.default.removeObserver(self,
name: notification,
object: nil)
}
stateMachineSubscriberToken = nil
}
func beginActivityTracking(_ listener: @escaping (ApplicationState) -> Void) {
stateMachineSubscriberToken = stateMachine.subscribe(listener)
}
private func beginBackgroundTracking() {
#if canImport(UIKit)
if backgroundTrackingTimeout > 0 {
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: Constants.backgroundTask) { [weak self] in
self?.stateMachine.process(.backgroundTrackingDidTimeout)
self?.stopBackgroundTracking()
}
}
#endif
guard backgroundTrackingTimeout != .infinity else { return }
backgroundTimer = Timer.scheduledTimer(withTimeInterval: backgroundTrackingTimeout, repeats: false) { [weak self] _ in
self?.stateMachine.process(.backgroundTrackingDidTimeout)
self?.stopBackgroundTracking()
}
}
private func stopBackgroundTracking() {
backgroundTimer = nil
#if canImport(UIKit)
guard backgroundTask != .invalid else {
return
}
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
#endif
}
@objc private func handleApplicationStateChange(_ notification: Notification) {
switch notification.name {
case Self.applicationDidMoveToBackgroundNotification:
beginBackgroundTracking()
stateMachine.process(.applicationDidMoveToBackground)
case Self.applicationWillMoveToForegoundNotification:
stopBackgroundTracking()
stateMachine.process(.applicationWillMoveToForeground)
case Self.applicationWillTerminateNotification:
stateMachine.process(.applicationWillTerminate)
default:
return
}
}
}
#if canImport(UIKit)
extension ActivityTracker {
struct Constants {
static let backgroundTask = "com.amazonaws.AWSPinpointSessionBackgroundTask"
}
}
#endif