diff --git a/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift b/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift index 5894f5e6b..95a7845d9 100644 --- a/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift +++ b/Sources/Core/InternalQueue/TrackerControllerIQWrapper.swift @@ -156,6 +156,11 @@ class TrackerControllerIQWrapper: TrackerController { get { return InternalQueue.sync { controller.platformContextProperties } } set { InternalQueue.sync { controller.platformContextProperties = newValue } } } + + var platformContextRetriever: PlatformContextRetriever? { + get { return InternalQueue.sync { controller.platformContextRetriever } } + set { InternalQueue.sync { controller.platformContextRetriever = newValue } } + } var geoLocationContext: Bool { get { return InternalQueue.sync { controller.geoLocationContext } } diff --git a/Sources/Core/Subject/PlatformContext.swift b/Sources/Core/Subject/PlatformContext.swift index 27b84d0ec..a1ba0090e 100644 --- a/Sources/Core/Subject/PlatformContext.swift +++ b/Sources/Core/Subject/PlatformContext.swift @@ -26,21 +26,27 @@ class PlatformContext { private var lastUpdatedEphemeralNetworkDict: TimeInterval = 0.0 private var deviceInfoMonitor: DeviceInfoMonitor + /// Overrides for retrieving property values + var platformContextRetriever: PlatformContextRetriever + /// List of properties of the platform context to track var platformContextProperties: [PlatformContextProperty]? /// Initializes a newly allocated PlatformContext object with custom update frequency for mobile and network properties and a custom device info monitor /// - Parameters: /// - platformContextProperties: List of properties of the platform context to track + /// - platformContextRetriever: Overrides for the property retrieving behavior /// - mobileDictUpdateFrequency: Minimal gap between subsequent updates of mobile platform information /// - networkDictUpdateFrequency: Minimal gap between subsequent updates of network platform information /// - deviceInfoMonitor: Device monitor for fetching platform information /// - Returns: a PlatformContext object init(platformContextProperties: [PlatformContextProperty]? = nil, + platformContextRetriever: PlatformContextRetriever? = nil, mobileDictUpdateFrequency: TimeInterval = 1.0, networkDictUpdateFrequency: TimeInterval = 10.0, deviceInfoMonitor: DeviceInfoMonitor = DeviceInfoMonitor()) { self.platformContextProperties = platformContextProperties + self.platformContextRetriever = platformContextRetriever ?? PlatformContextRetriever() self.mobileDictUpdateFrequency = mobileDictUpdateFrequency self.networkDictUpdateFrequency = networkDictUpdateFrequency self.deviceInfoMonitor = deviceInfoMonitor @@ -52,7 +58,7 @@ class PlatformContext { /// Updates and returns payload dictionary with device context information. /// - Parameter userAnonymisation: Whether to anonymise user identifiers (IDFA values) - func fetchPlatformDict(userAnonymisation: Bool, advertisingIdentifierRetriever: (() -> UUID?)?) -> Payload { + func fetchPlatformDict(userAnonymisation: Bool) -> Payload { #if os(iOS) || os(visionOS) let now = Date().timeIntervalSince1970 if now - lastUpdatedEphemeralMobileDict >= mobileDictUpdateFrequency { @@ -69,10 +75,8 @@ class PlatformContext { copy[kSPMobileAppleIdfv] = nil return copy } else { - if let retriever = advertisingIdentifierRetriever { - if shouldTrack(.appleIdfa) && platformDict.dictionary[kSPMobileAppleIdfa] == nil { - platformDict[kSPMobileAppleIdfa] = retriever()?.uuidString - } + if shouldTrack(.appleIdfa) && platformDict.dictionary[kSPMobileAppleIdfa] == nil { + platformDict[kSPMobileAppleIdfa] = platformContextRetriever.appleIdfa?()?.uuidString } return platformDict } @@ -82,10 +86,22 @@ class PlatformContext { func setPlatformDict() { platformDict = Payload() - platformDict[kSPPlatformOsType] = deviceInfoMonitor.osType - platformDict[kSPPlatformOsVersion] = deviceInfoMonitor.osVersion - platformDict[kSPPlatformDeviceManu] = deviceInfoMonitor.deviceVendor - platformDict[kSPPlatformDeviceModel] = deviceInfoMonitor.deviceModel + platformDict[kSPPlatformOsType] = ( + platformContextRetriever.osType == nil ? + deviceInfoMonitor.osType : platformContextRetriever.osType?() + ) + platformDict[kSPPlatformOsVersion] = ( + platformContextRetriever.osVersion == nil ? + deviceInfoMonitor.osVersion : platformContextRetriever.osVersion?() + ) + platformDict[kSPPlatformDeviceManu] = ( + platformContextRetriever.deviceVendor == nil ? + deviceInfoMonitor.deviceVendor : platformContextRetriever.deviceVendor?() + ) + platformDict[kSPPlatformDeviceModel] = ( + platformContextRetriever.deviceModel == nil ? + deviceInfoMonitor.deviceModel : platformContextRetriever.deviceModel?() + ) #if os(iOS) || os(visionOS) setMobileDict() @@ -94,20 +110,39 @@ class PlatformContext { func setMobileDict() { if shouldTrack(.resolution) { - platformDict[kSPMobileResolution] = deviceInfoMonitor.resolution + platformDict[kSPMobileResolution] = ( + platformContextRetriever.resolution == nil ? + deviceInfoMonitor.resolution : platformContextRetriever.resolution?() + ) } if shouldTrack(.language) { // the schema has a max-length 8 for language which iOS exceeds sometimes - if let language = deviceInfoMonitor.language { platformDict[kSPMobileLanguage] = String(language.prefix(8)) } + let language = ( + platformContextRetriever.language == nil ? + deviceInfoMonitor.language : platformContextRetriever.language?() + ) + if let language = language { platformDict[kSPMobileLanguage] = String(language.prefix(8)) } } if shouldTrack(.scale) { - platformDict[kSPMobileScale] = deviceInfoMonitor.scale + platformDict[kSPMobileScale] = ( + platformContextRetriever.scale == nil ? + deviceInfoMonitor.scale : platformContextRetriever.scale?() + ) } if shouldTrack(.carrier) { - platformDict[kSPMobileCarrier] = deviceInfoMonitor.carrierName + platformDict[kSPMobileCarrier] = ( + platformContextRetriever.carrier == nil ? + deviceInfoMonitor.carrierName : platformContextRetriever.carrier?() + ) + } + if shouldTrack(.totalStorage) { + platformDict[kSPMobileTotalStorage] = platformContextRetriever.totalStorage?() } if shouldTrack(.physicalMemory) { - platformDict[kSPMobilePhysicalMemory] = deviceInfoMonitor.physicalMemory + platformDict[kSPMobilePhysicalMemory] = ( + platformContextRetriever.physicalMemory == nil ? + deviceInfoMonitor.physicalMemory : platformContextRetriever.physicalMemory?() + ) } setEphemeralMobileDict() @@ -118,23 +153,44 @@ class PlatformContext { lastUpdatedEphemeralMobileDict = Date().timeIntervalSince1970 if shouldTrack(.appleIdfv) && platformDict[kSPMobileAppleIdfv] == nil { - platformDict[kSPMobileAppleIdfv] = deviceInfoMonitor.appleIdfv + platformDict[kSPMobileAppleIdfv] = ( + platformContextRetriever.appleIdfv == nil ? + deviceInfoMonitor.appleIdfv : platformContextRetriever.appleIdfv?() + ) } if shouldTrack(.batteryLevel) { - platformDict[kSPMobileBatteryLevel] = deviceInfoMonitor.batteryLevel + platformDict[kSPMobileBatteryLevel] = ( + platformContextRetriever.batteryLevel == nil ? + deviceInfoMonitor.batteryLevel : platformContextRetriever.batteryLevel?() + ) } if shouldTrack(.batteryState) { - platformDict[kSPMobileBatteryState] = deviceInfoMonitor.batteryState + platformDict[kSPMobileBatteryState] = ( + platformContextRetriever.batteryState == nil ? + deviceInfoMonitor.batteryState : platformContextRetriever.batteryState?() + ) } if shouldTrack(.lowPowerMode) { - platformDict[kSPMobileLowPowerMode] = deviceInfoMonitor.isLowPowerModeEnabled + platformDict[kSPMobileLowPowerMode] = ( + platformContextRetriever.lowPowerMode == nil ? + deviceInfoMonitor.isLowPowerModeEnabled : platformContextRetriever.lowPowerMode?() + ) + } + if shouldTrack(.availableStorage) { + platformDict[kSPMobileAvailableStorage] = platformContextRetriever.availableStorage?() } if shouldTrack(.appAvailableMemory) { - platformDict[kSPMobileAppAvailableMemory] = deviceInfoMonitor.appAvailableMemory + platformDict[kSPMobileAppAvailableMemory] = ( + platformContextRetriever.appAvailableMemory == nil ? + deviceInfoMonitor.appAvailableMemory : platformContextRetriever.appAvailableMemory?() + ) } if shouldTrack(.isPortrait) { - platformDict[kSPMobileIsPortrait] = deviceInfoMonitor.isPortrait + platformDict[kSPMobileIsPortrait] = ( + platformContextRetriever.isPortrait == nil ? + deviceInfoMonitor.isPortrait : platformContextRetriever.isPortrait?() + ) } } @@ -142,10 +198,16 @@ class PlatformContext { lastUpdatedEphemeralNetworkDict = Date().timeIntervalSince1970 if shouldTrack(.networkTechnology) { - platformDict[kSPMobileNetworkTech] = deviceInfoMonitor.networkTechnology + platformDict[kSPMobileNetworkTech] = ( + platformContextRetriever.networkTechnology == nil ? + deviceInfoMonitor.networkTechnology : platformContextRetriever.networkTechnology?() + ) } if shouldTrack(.networkType) { - platformDict[kSPMobileNetworkType] = deviceInfoMonitor.networkType + platformDict[kSPMobileNetworkType] = ( + platformContextRetriever.networkType == nil ? + deviceInfoMonitor.networkType : platformContextRetriever.networkType?() + ) } } diff --git a/Sources/Core/Subject/Subject.swift b/Sources/Core/Subject/Subject.swift index 610e4ee10..d878406c1 100644 --- a/Sources/Core/Subject/Subject.swift +++ b/Sources/Core/Subject/Subject.swift @@ -17,17 +17,30 @@ import Foundation /// This class is used to access and persist user information, it represents the current user being tracked. class Subject : NSObject { private var standardDict: [String : String] = [:] - private var platformContextManager: PlatformContext private var geoDict: [String : NSObject] = [:] + + var geoLocationContext = false + + // MARK: - Platform context + private var platformContextManager: PlatformContext + var platformContext = false var platformContextProperties: [PlatformContextProperty]? { get { return platformContextManager.platformContextProperties } set { platformContextManager.platformContextProperties = newValue } } + + var platformContextRetriever: PlatformContextRetriever { + get { return platformContextManager.platformContextRetriever } + set { platformContextManager.platformContextRetriever = newValue } + } - var geoLocationContext = false + var advertisingIdentifierRetriever: (() -> UUID?)? { + get { return platformContextManager.platformContextRetriever.appleIdfa } + set { platformContextManager.platformContextRetriever.appleIdfa = newValue } + } // MARK: - Standard Dictionary @@ -194,9 +207,13 @@ class Subject : NSObject { init(platformContext: Bool = false, platformContextProperties: [PlatformContextProperty]? = nil, + platformContextRetriever: PlatformContextRetriever? = nil, geoLocationContext geoContext: Bool = false, subjectConfiguration config: SubjectConfiguration? = nil) { - self.platformContextManager = PlatformContext(platformContextProperties: platformContextProperties) + self.platformContextManager = PlatformContext( + platformContextProperties: platformContextProperties, + platformContextRetriever: platformContextRetriever + ) super.init() platformContextManager.platformContextProperties = platformContextProperties self.platformContext = platformContext @@ -252,11 +269,9 @@ class Subject : NSObject { /// Gets all platform dictionary pairs to decorate event with. Returns nil if not enabled. /// - Parameter userAnonymisation: Whether to anonymise user identifiers /// - Returns: A SPPayload with all platform specific pairs. - func platformDict(userAnonymisation: Bool, advertisingIdentifierRetriever: (() -> UUID?)?) -> Payload? { + func platformDict(userAnonymisation: Bool) -> Payload? { if platformContext { - return platformContextManager.fetchPlatformDict( - userAnonymisation: userAnonymisation, - advertisingIdentifierRetriever: advertisingIdentifierRetriever) + return platformContextManager.fetchPlatformDict(userAnonymisation: userAnonymisation) } else { return nil } diff --git a/Sources/Core/Tracker/ServiceProvider.swift b/Sources/Core/Tracker/ServiceProvider.swift index f8c6cc482..9323800be 100644 --- a/Sources/Core/Tracker/ServiceProvider.swift +++ b/Sources/Core/Tracker/ServiceProvider.swift @@ -227,6 +227,7 @@ class ServiceProvider: NSObject, ServiceProviderProtocol { return Subject( platformContext: trackerConfiguration.platformContext, platformContextProperties: trackerConfiguration.platformContextProperties, + platformContextRetriever: trackerConfiguration.platformContextRetriever, geoLocationContext: trackerConfiguration.geoLocationContext, subjectConfiguration: subjectConfiguration) } @@ -304,7 +305,6 @@ class ServiceProvider: NSObject, ServiceProviderProtocol { tracker.trackerDiagnostic = trackerConfiguration.diagnosticAutotracking tracker.userAnonymisation = trackerConfiguration.userAnonymisation tracker.immersiveSpaceContext = trackerConfiguration.immersiveSpaceContext - tracker.advertisingIdentifierRetriever = trackerConfiguration.advertisingIdentifierRetriever if gdprConfiguration.sourceConfig != nil { tracker.gdprContext = GDPRContext( basis: gdprConfiguration.basisForProcessing, diff --git a/Sources/Core/Tracker/Tracker.swift b/Sources/Core/Tracker/Tracker.swift index 2bd492bfc..93a9f9329 100644 --- a/Sources/Core/Tracker/Tracker.swift +++ b/Sources/Core/Tracker/Tracker.swift @@ -261,8 +261,6 @@ class Tracker: NSObject { var isTracking: Bool { return dataCollection } - - var advertisingIdentifierRetriever: (() -> UUID?)? init(trackerNamespace: String, appId: String?, @@ -558,9 +556,7 @@ class Tracker: NSObject { func addBasicContexts(event: TrackerEvent) { if subject != nil { - if let platformDict = subject?.platformDict( - userAnonymisation: userAnonymisation, - advertisingIdentifierRetriever: advertisingIdentifierRetriever)?.dictionary { + if let platformDict = subject?.platformDict(userAnonymisation: userAnonymisation)?.dictionary { event.addContextEntity(SelfDescribingJson(schema: platformContextSchema, andDictionary: platformDict)) } if let geoLocationDict = subject?.geoLocationDict { diff --git a/Sources/Core/Tracker/TrackerControllerImpl.swift b/Sources/Core/Tracker/TrackerControllerImpl.swift index 588deac8d..f4b6822df 100644 --- a/Sources/Core/Tracker/TrackerControllerImpl.swift +++ b/Sources/Core/Tracker/TrackerControllerImpl.swift @@ -161,6 +161,11 @@ class TrackerControllerImpl: Controller, TrackerController { tracker.subject?.platformContextProperties = newValue } } + + var platformContextRetriever: PlatformContextRetriever? { + get { return tracker.subject?.platformContextRetriever } + set { if let retriever = newValue { tracker.subject?.platformContextRetriever = retriever } } + } var geoLocationContext: Bool { get { @@ -294,11 +299,11 @@ class TrackerControllerImpl: Controller, TrackerController { var advertisingIdentifierRetriever: (() -> UUID?)? { get { - return tracker.advertisingIdentifierRetriever + return tracker.subject?.advertisingIdentifierRetriever } set { dirtyConfig.advertisingIdentifierRetriever = newValue - tracker.advertisingIdentifierRetriever = newValue + tracker.subject?.advertisingIdentifierRetriever = newValue } } diff --git a/Sources/Core/TrackerConstants.swift b/Sources/Core/TrackerConstants.swift index 063aa7600..3fa3e1a05 100644 --- a/Sources/Core/TrackerConstants.swift +++ b/Sources/Core/TrackerConstants.swift @@ -136,6 +136,8 @@ let kSPMobileAppAvailableMemory = "appAvailableMemory" let kSPMobileBatteryLevel = "batteryLevel" let kSPMobileBatteryState = "batteryState" let kSPMobileLowPowerMode = "lowPowerMode" +let kSPMobileAvailableStorage = "availableStorage" +let kSPMobileTotalStorage = "totalStorage" let kSPMobileIsPortrait = "isPortrait" let kSPMobileResolution = "resolution" let kSPMobileLanguage = "language" diff --git a/Sources/Core/Utils/DeviceInfoMonitor.swift b/Sources/Core/Utils/DeviceInfoMonitor.swift index bbf7f05f0..75d8c0219 100644 --- a/Sources/Core/Utils/DeviceInfoMonitor.swift +++ b/Sources/Core/Utils/DeviceInfoMonitor.swift @@ -38,15 +38,15 @@ class DeviceInfoMonitor { /// Returns the current device's vendor in the form of a string. /// - Returns: A string with vendor, i.e. "Apple Inc." - var deviceVendor: String? { + var deviceVendor: String { return "Apple Inc." } /// Returns the current device's model in the form of a string. /// - Returns: A string with device model. - var deviceModel: String? { + var deviceModel: String { let simulatorModel = (ProcessInfo.processInfo.environment)["SIMULATOR_MODEL_IDENTIFIER"] - if simulatorModel != nil { + if let simulatorModel = simulatorModel { return simulatorModel } @@ -59,7 +59,7 @@ class DeviceInfoMonitor { /// This is to detect what the version of mobile OS of the current device. /// - Returns: The current device's OS version type as a string. - var osVersion: String? { + var osVersion: String { #if os(iOS) || os(tvOS) || os(visionOS) return UIDevice.current.systemVersion #elseif os(watchOS) @@ -78,7 +78,7 @@ class DeviceInfoMonitor { #endif } - var osType: String? { + var osType: String { #if os(iOS) return "ios" #elseif os(tvOS) diff --git a/Sources/Snowplow/Configurations/PlatformContextProperty.swift b/Sources/Snowplow/Configurations/PlatformContextProperty.swift index 5c5053b4e..c2debc852 100644 --- a/Sources/Snowplow/Configurations/PlatformContextProperty.swift +++ b/Sources/Snowplow/Configurations/PlatformContextProperty.swift @@ -13,35 +13,41 @@ import Foundation -/// Optional properties tracked in the platform context entity +/// Optional properties tracked in the platform context entity. public enum PlatformContextProperty: Int { - /// The carrier of the SIM inserted in the device + /// The carrier of the SIM inserted in the device. case carrier - /// Type of network the device is connected to + /// Type of network the device is connected to. case networkType - /// Radio access technology that the device is using + /// Radio access technology that the device is using. case networkTechnology - /// Advertising identifier on iOS + /// Advertising identifier on iOS. case appleIdfa - /// UUID identifier for vendors on iOS + /// UUID identifier for vendors on iOS. case appleIdfv - /// Total physical system memory in bytes + /// Total physical system memory in bytes. case physicalMemory - /// Amount of memory in bytes available to the current app + /// Amount of memory in bytes available to the current app. /// The property is not tracked in the current version of the tracker due to the tracker not being able to access the API, see the issue here: https://github.com/snowplow/snowplow-ios-tracker/issues/772 case appAvailableMemory - /// Remaining battery level as an integer percentage of total battery capacity + /// Remaining battery level as an integer percentage of total battery capacity. case batteryLevel - /// Battery state for the device + /// Battery state for the device. case batteryState - /// A Boolean indicating whether Low Power Mode is enabled + /// A Boolean indicating whether Low Power Mode is enabled. case lowPowerMode - /// A Boolean indicating whether the device orientation is portrait (either upright or upside down) + /// Bytes of storage remaining. + /// Note: This is not automatically assigned by the tracker as it may be considered as fingerprinting. You can assign it using the PlatformContextRetriever. + case availableStorage + /// Total size of storage in bytes + /// Note: This is not automatically assigned by the tracker as it may be considered as fingerprinting. You can assign it using the PlatformContextRetriever. + case totalStorage + /// A Boolean indicating whether the device orientation is portrait (either upright or upside down). case isPortrait - /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes + /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes. case resolution - /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS) + /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS). case scale - /// System language currently used on the device (ISO 639) + /// System language currently used on the device (ISO 639). case language } diff --git a/Sources/Snowplow/Configurations/TrackerConfiguration.swift b/Sources/Snowplow/Configurations/TrackerConfiguration.swift index 3e8e6b949..ae441d611 100644 --- a/Sources/Snowplow/Configurations/TrackerConfiguration.swift +++ b/Sources/Snowplow/Configurations/TrackerConfiguration.swift @@ -89,6 +89,10 @@ public protocol PlatformContextConfigurationProtocol { /// List of properties of the platform context to track. If not passed and `platformContext` is enabled, all available properties will be tracked. /// The required `osType`, `osVersion`, `deviceManufacturer`, and `deviceModel` properties will be tracked in the entity regardless of this setting. var platformContextProperties: [PlatformContextProperty]? { get set } + + /// Set of callbacks to be used to retrieve properties of the platform context. + /// Overrides the tracker implementation for setting the properties. + var platformContextRetriever: PlatformContextRetriever? { get set } } /// This class represents the configuration of the tracker and the core tracker properties. @@ -264,8 +268,14 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati /// It is called repeatedly (on each tracked event) until a UUID is returned. @objc public var advertisingIdentifierRetriever: (() -> UUID?)? { - get { return _advertisingIdentifierRetriever ?? sourceConfig?.advertisingIdentifierRetriever } - set { _advertisingIdentifierRetriever = newValue } + get { return platformContextRetriever?.appleIdfa } + set { + if let retriever = platformContextRetriever { + retriever.appleIdfa = newValue + } else { + platformContextRetriever = PlatformContextRetriever(appleIdfa: newValue) + } + } } private var _platformContextProperties: [PlatformContextProperty]? @@ -276,6 +286,14 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati set { _platformContextProperties = newValue } } + private var _platformContextRetriever: PlatformContextRetriever? + /// Set of callbacks to be used to retrieve properties of the platform context. + /// Overrides the tracker implementation for setting the properties. + public var platformContextRetriever: PlatformContextRetriever? { + get { return _platformContextRetriever ?? sourceConfig?.platformContextRetriever } + set { _platformContextRetriever = newValue } + } + // MARK: - Internal /// Fallback configuration to read from in case requested values are not present in this configuraiton. @@ -518,6 +536,13 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati self.advertisingIdentifierRetriever = retriever return self } + + /// Set of callbacks to be used to retrieve properties of the platform context. + /// Overrides the tracker implementation for setting the properties. + public func platformContextRetriever(_ retriever: PlatformContextRetriever?) -> Self { + self.platformContextRetriever = retriever + return self + } // MARK: - NSCopying @@ -533,6 +558,7 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati copy.applicationContext = applicationContext copy.platformContext = platformContext copy.platformContextProperties = platformContextProperties + copy.platformContextRetriever = platformContextRetriever copy.geoLocationContext = geoLocationContext copy.deepLinkContext = deepLinkContext copy.screenContext = screenContext @@ -545,7 +571,6 @@ public class TrackerConfiguration: SerializableConfiguration, TrackerConfigurati copy.trackerVersionSuffix = trackerVersionSuffix copy.userAnonymisation = userAnonymisation copy.immersiveSpaceContext = immersiveSpaceContext - copy.advertisingIdentifierRetriever = advertisingIdentifierRetriever return copy } diff --git a/Sources/Snowplow/Tracker/PlaformContextRetriever.swift b/Sources/Snowplow/Tracker/PlaformContextRetriever.swift new file mode 100644 index 000000000..4f5fc2367 --- /dev/null +++ b/Sources/Snowplow/Tracker/PlaformContextRetriever.swift @@ -0,0 +1,143 @@ +// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License +// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at +// http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the Apache License Version 2.0 for the specific +// language governing permissions and limitations there under. + +import Foundation + +/// Overrides for the values for properties of the platform context. +public class PlatformContextRetriever { + + /// Operating system type (e.g., ios, tvos, watchos, osx, android) + public var osType: (() -> String)? = nil + + /// The current version of the operating system + public var osVersion: (() -> String)? = nil + + /// The manufacturer of the product/hardware + public var deviceVendor: (() -> String)? = nil + + /// The end-user-visible name for the end product + public var deviceModel: (() -> String)? = nil + + /// The carrier of the SIM inserted in the device + public var carrier: (() -> String?)? = nil + + /// Type of network the device is connected to + public var networkType: (() -> String?)? = nil + + /// Radio access technology that the device is using + public var networkTechnology: (() -> String?)? = nil + + /// Advertising identifier on iOS + public var appleIdfa: (() -> UUID?)? = nil + + /// UUID identifier for vendors on iOS + public var appleIdfv: (() -> String?)? = nil + + /// Bytes of storage remaining + public var availableStorage: (() -> Int64?)? = nil + + /// Total size of storage in bytes + public var totalStorage: (() -> Int64?)? = nil + + /// Total physical system memory in bytes + public var physicalMemory: (() -> UInt64?)? = nil + + /// Amount of memory in bytes available to the current app + public var appAvailableMemory: (() -> Int?)? = nil + + /// Remaining battery level as an integer percentage of total battery capacity + public var batteryLevel: (() -> Int?)? = nil + + /// Battery state for the device + public var batteryState: (() -> String?)? = nil + + /// A Boolean indicating whether Low Power Mode is enabled + public var lowPowerMode: (() -> Bool?)? = nil + + /// A Boolean indicating whether the device orientation is portrait (either upright or upside down) + public var isPortrait: (() -> Bool?)? = nil + + /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes + public var resolution: (() -> String?)? = nil + + /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS) + public var scale: (() -> Double?)? = nil + + /// System language currently used on the device (ISO 639) + public var language: (() -> String)? = nil + + /// - Parameters: + /// - osType: Operating system type (e.g., ios, tvos, watchos, osx, android) + /// - osVersion: The current version of the operating system + /// - deviceVendor: The manufacturer of the product/hardware + /// - deviceModel: The end-user-visible name for the end product + /// - carrier: The carrier of the SIM inserted in the device + /// - networkType: Type of network the device is connected to + /// - networkTechnology: Radio access technology that the device is using + /// - appleIdfa: Advertising identifier on iOS + /// - appleIdfv: UUID identifier for vendors on iOS + /// - availableStorage: Bytes of storage remaining + /// - totalStorage: Total size of storage in bytes + /// - physicalMemory: Total physical system memory in bytes + /// - appAvailableMemory: Amount of memory in bytes available to the current app + /// - batteryLevel: Remaining battery level as an integer percentage of total battery capacity + /// - batteryState: Battery state for the device + /// - lowPowerMode: A Boolean indicating whether Low Power Mode is enabled + /// - isPortrait: A Boolean indicating whether the device orientation is portrait (either upright or upside down) + /// - resolution: Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes + /// - scale: Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS) + /// - language: System language currently used on the device (ISO 639) + public init( + osType: (() -> String)? = nil, + osVersion: (() -> String)? = nil, + deviceVendor: (() -> String)? = nil, + deviceModel: (() -> String)? = nil, + carrier: (() -> String?)? = nil, + networkType: (() -> String?)? = nil, + networkTechnology: (() -> String?)? = nil, + appleIdfa: (() -> UUID?)? = nil, + appleIdfv: (() -> String?)? = nil, + availableStorage: (() -> Int64?)? = nil, + totalStorage: (() -> Int64?)? = nil, + physicalMemory: (() -> UInt64?)? = nil, + appAvailableMemory: (() -> Int?)? = nil, + batteryLevel: (() -> Int?)? = nil, + batteryState: (() -> String?)? = nil, + lowPowerMode: (() -> Bool?)? = nil, + isPortrait: (() -> Bool?)? = nil, + resolution: (() -> String?)? = nil, + scale: (() -> Double?)? = nil, + language: (() -> String)? = nil + ) { + self.osType = osType + self.osVersion = osVersion + self.deviceVendor = deviceVendor + self.deviceModel = deviceModel + self.carrier = carrier + self.networkType = networkType + self.networkTechnology = networkTechnology + self.appleIdfa = appleIdfa + self.appleIdfv = appleIdfv + self.availableStorage = availableStorage + self.totalStorage = totalStorage + self.physicalMemory = physicalMemory + self.appAvailableMemory = appAvailableMemory + self.batteryLevel = batteryLevel + self.batteryState = batteryState + self.lowPowerMode = lowPowerMode + self.isPortrait = isPortrait + self.resolution = resolution + self.scale = scale + self.language = language + } +} diff --git a/Tests/Legacy Tests/LegacyTestSubject.swift b/Tests/Legacy Tests/LegacyTestSubject.swift index 8fe7366ae..126240429 100644 --- a/Tests/Legacy Tests/LegacyTestSubject.swift +++ b/Tests/Legacy Tests/LegacyTestSubject.swift @@ -33,7 +33,7 @@ class LegacyTestSubject: XCTestCase { func testSubjectInitWithOptions() { let subject = Subject(platformContext: true, geoLocationContext: false) - XCTAssertNotNil(subject.platformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil)) + XCTAssertNotNil(subject.platformDict(userAnonymisation: false)) XCTAssertNotNil(subject.standardDict(userAnonymisation: false)) } diff --git a/Tests/TestPlatformContext.swift b/Tests/TestPlatformContext.swift index 0b3f300b3..9736b3285 100644 --- a/Tests/TestPlatformContext.swift +++ b/Tests/TestPlatformContext.swift @@ -17,23 +17,24 @@ import XCTest class TestPlatformContext: XCTestCase { func testContainsPlatformInfo() { let context = PlatformContext(deviceInfoMonitor: MockDeviceInfoMonitor()) - let platformDict = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil).dictionary + let platformDict = context.fetchPlatformDict(userAnonymisation: false).dictionary XCTAssertNotNil(platformDict) XCTAssertNotNil(platformDict) } func testContainsMobileInfo() { let context = PlatformContext(deviceInfoMonitor: MockDeviceInfoMonitor()) - let platformDict = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil).dictionary + let platformDict = context.fetchPlatformDict(userAnonymisation: false).dictionary XCTAssertNotNil(platformDict) XCTAssertNotNil(platformDict) } func testAddsAllMockedInfo() { let deviceInfoMonitor = MockDeviceInfoMonitor() - let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) let idfa = UUID() - let platformDict = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: { idfa }) + let retriever = PlatformContextRetriever(appleIdfa: { idfa }) + let context = PlatformContext(platformContextRetriever: retriever, mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) + let platformDict = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(idfa.uuidString, platformDict[kSPMobileAppleIdfa] as? String) XCTAssertEqual("Apple Inc.", platformDict[kSPPlatformDeviceManu] as? String) XCTAssertEqual("deviceModel", platformDict[kSPPlatformDeviceModel] as? String) @@ -59,10 +60,10 @@ class TestPlatformContext: XCTestCase { let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(2, deviceInfoMonitor.accessCount("batteryLevel")) XCTAssertEqual(2, deviceInfoMonitor.accessCount("appAvailableMemory")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(3, deviceInfoMonitor.accessCount("batteryLevel")) XCTAssertEqual(3, deviceInfoMonitor.accessCount("appAvailableMemory")) } @@ -72,10 +73,10 @@ class TestPlatformContext: XCTestCase { let context = PlatformContext(mobileDictUpdateFrequency: 1000, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(1, deviceInfoMonitor.accessCount("batteryLevel")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("appAvailableMemory")) } @@ -85,10 +86,10 @@ class TestPlatformContext: XCTestCase { let context = PlatformContext(mobileDictUpdateFrequency: 1, networkDictUpdateFrequency: 0, deviceInfoMonitor: deviceInfoMonitor) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(2, deviceInfoMonitor.accessCount("networkTechnology")) XCTAssertEqual(2, deviceInfoMonitor.accessCount("networkType")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(3, deviceInfoMonitor.accessCount("networkTechnology")) XCTAssertEqual(3, deviceInfoMonitor.accessCount("networkType")) } @@ -98,10 +99,10 @@ class TestPlatformContext: XCTestCase { let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1000, deviceInfoMonitor: deviceInfoMonitor) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkTechnology")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("networkType")) } @@ -111,10 +112,10 @@ class TestPlatformContext: XCTestCase { let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 0, deviceInfoMonitor: deviceInfoMonitor) XCTAssertEqual(1, deviceInfoMonitor.accessCount("physicalMemory")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("carrierName")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(1, deviceInfoMonitor.accessCount("physicalMemory")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("carrierName")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(1, deviceInfoMonitor.accessCount("physicalMemory")) XCTAssertEqual(1, deviceInfoMonitor.accessCount("carrierName")) } @@ -123,7 +124,7 @@ class TestPlatformContext: XCTestCase { let deviceInfoMonitor = MockDeviceInfoMonitor() let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) XCTAssertEqual(1, deviceInfoMonitor.accessCount("appleIdfv")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(1, deviceInfoMonitor.accessCount("appleIdfv")) } @@ -132,53 +133,61 @@ class TestPlatformContext: XCTestCase { deviceInfoMonitor.customAppleIdfv = nil let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) XCTAssertEqual(1, deviceInfoMonitor.accessCount("appleIdfv")) - _ = context.fetchPlatformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + _ = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(2, deviceInfoMonitor.accessCount("appleIdfv")) } func testUpdatesIdfaIfNil() { let deviceInfoMonitor = MockDeviceInfoMonitor() - let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) - - let platformDict1 = context.fetchPlatformDict( - userAnonymisation: false, - advertisingIdentifierRetriever: { nil } + var idfa: UUID? = nil + let retriever = PlatformContextRetriever(appleIdfa: { idfa }) + let context = PlatformContext( + platformContextRetriever: retriever, + mobileDictUpdateFrequency: 0, + networkDictUpdateFrequency: 1, + deviceInfoMonitor: deviceInfoMonitor ) + + let platformDict1 = context.fetchPlatformDict(userAnonymisation: false) XCTAssertNil(platformDict1[kSPMobileAppleIdfa]) - let idfa = UUID() - let platformDict2 = context.fetchPlatformDict( - userAnonymisation: false, - advertisingIdentifierRetriever: { idfa } - ) - XCTAssertEqual(idfa.uuidString, platformDict2[kSPMobileAppleIdfa] as? String) + idfa = UUID() + let platformDict2 = context.fetchPlatformDict(userAnonymisation: false) + XCTAssertEqual(idfa?.uuidString, platformDict2[kSPMobileAppleIdfa] as? String) } func testDoesntUpdateIdfaIfAlreadyRetrieved() { let deviceInfoMonitor = MockDeviceInfoMonitor() - let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) let idfa1 = UUID() - let platformDict1 = context.fetchPlatformDict( - userAnonymisation: false, - advertisingIdentifierRetriever: { idfa1 } + var idfa = idfa1 + + let retriever = PlatformContextRetriever(appleIdfa: { idfa }) + let context = PlatformContext( + platformContextRetriever: retriever, + mobileDictUpdateFrequency: 0, + networkDictUpdateFrequency: 1, + deviceInfoMonitor: deviceInfoMonitor ) + + let platformDict1 = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(idfa1.uuidString, platformDict1[kSPMobileAppleIdfa] as? String) - let platformDict2 = context.fetchPlatformDict( - userAnonymisation: false, - advertisingIdentifierRetriever: { UUID() } - ) + idfa = UUID() + let platformDict2 = context.fetchPlatformDict(userAnonymisation: false) XCTAssertEqual(idfa1.uuidString, platformDict2[kSPMobileAppleIdfa] as? String) } func testAnonymisesUserIdentifiers() { let deviceInfoMonitor = MockDeviceInfoMonitor() - let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) - let platformDict = context.fetchPlatformDict( - userAnonymisation: true, - advertisingIdentifierRetriever: { UUID() } + let retriever = PlatformContextRetriever(appleIdfa: { UUID() }) + let context = PlatformContext( + platformContextRetriever: retriever, + mobileDictUpdateFrequency: 0, + networkDictUpdateFrequency: 1, + deviceInfoMonitor: deviceInfoMonitor ) + let platformDict = context.fetchPlatformDict(userAnonymisation: true) XCTAssertNil(platformDict[kSPMobileAppleIdfa]) XCTAssertNil(platformDict[kSPMobileAppleIdfv]) } @@ -187,25 +196,21 @@ class TestPlatformContext: XCTestCase { let deviceInfoMonitor = MockDeviceInfoMonitor() deviceInfoMonitor.language = "1234567890" let context = PlatformContext(mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) - let platformDict = context.fetchPlatformDict( - userAnonymisation: true, - advertisingIdentifierRetriever: { UUID() } - ) + let platformDict = context.fetchPlatformDict(userAnonymisation: true) XCTAssertEqual("12345678", platformDict[kSPMobileLanguage] as? String) } #endif func testOnlyAddsRequestedProperties() { let deviceInfoMonitor = MockDeviceInfoMonitor() + let retriever = PlatformContextRetriever(appleIdfa: { UUID() }) let context = PlatformContext( platformContextProperties: [.appAvailableMemory, .language], + platformContextRetriever: retriever, mobileDictUpdateFrequency: 0, networkDictUpdateFrequency: 1, deviceInfoMonitor: deviceInfoMonitor) - let platformDict = context.fetchPlatformDict( - userAnonymisation: false, - advertisingIdentifierRetriever: { UUID() } - ) + let platformDict = context.fetchPlatformDict(userAnonymisation: false) XCTAssertNotNil(platformDict[kSPPlatformDeviceManu]) #if os(iOS) @@ -216,6 +221,61 @@ class TestPlatformContext: XCTestCase { XCTAssertNil(platformDict[kSPMobilePhysicalMemory]) XCTAssertNil(platformDict[kSPMobileIsPortrait]) XCTAssertNil(platformDict[kSPMobileAppleIdfa]) +#endif + } + + func testPlatformContextRetrieverOverridesProperties() { + let deviceInfoMonitor = MockDeviceInfoMonitor() + let idfa = UUID() + let retriever = PlatformContextRetriever( + osType: { "r1" }, + osVersion: { "r2" }, + deviceVendor: { "r3" }, + deviceModel: { "r4" }, + carrier: { "r5" }, + networkType: { "r6" }, + networkTechnology: { "r7" }, + appleIdfa: { idfa }, + appleIdfv: { "r9" }, + availableStorage: { 100 }, + totalStorage: { 101 }, + physicalMemory: { 102 }, + appAvailableMemory: { 103 }, + batteryLevel: { 104 }, + batteryState: { "r10" }, + lowPowerMode: { true }, + isPortrait: { false }, + resolution: { "r11" }, + scale: { 105 }, + language: { "r12" } + ) + let context = PlatformContext( + platformContextRetriever: retriever, + deviceInfoMonitor: deviceInfoMonitor) + let platformDict = context.fetchPlatformDict(userAnonymisation: false) + + XCTAssertEqual(platformDict[kSPPlatformOsType] as? String, "r1") + XCTAssertEqual(platformDict[kSPPlatformOsVersion] as? String, "r2") + XCTAssertEqual(platformDict[kSPPlatformDeviceManu] as? String, "r3") + XCTAssertEqual(platformDict[kSPPlatformDeviceModel] as? String, "r4") + +#if os(iOS) || os(visionOS) + XCTAssertEqual(platformDict[kSPMobileCarrier] as? String, "r5") + XCTAssertEqual(platformDict[kSPMobileNetworkType] as? String, "r6") + XCTAssertEqual(platformDict[kSPMobileNetworkTech] as? String, "r7") + XCTAssertEqual(platformDict[kSPMobileAppleIdfa] as? String, idfa.uuidString) + XCTAssertEqual(platformDict[kSPMobileAppleIdfv] as? String, "r9") + XCTAssertEqual(platformDict[kSPMobileAvailableStorage] as? Int64, 100) + XCTAssertEqual(platformDict[kSPMobileTotalStorage] as? Int64, 101) + XCTAssertEqual(platformDict[kSPMobilePhysicalMemory] as? UInt64, 102) + XCTAssertEqual(platformDict[kSPMobileAppAvailableMemory] as? Int, 103) + XCTAssertEqual(platformDict[kSPMobileBatteryLevel] as? Int, 104) + XCTAssertEqual(platformDict[kSPMobileBatteryState] as? String, "r10") + XCTAssertEqual(platformDict[kSPMobileLowPowerMode] as? Bool, true) + XCTAssertEqual(platformDict[kSPMobileIsPortrait] as? Bool, false) + XCTAssertEqual(platformDict[kSPMobileResolution] as? String, "r11") + XCTAssertEqual(platformDict[kSPMobileScale] as? Double, 105) + XCTAssertEqual(platformDict[kSPMobileLanguage] as? String, "r12") #endif } } diff --git a/Tests/TestSubject.swift b/Tests/TestSubject.swift index 36e163f14..08e32975d 100644 --- a/Tests/TestSubject.swift +++ b/Tests/TestSubject.swift @@ -17,14 +17,14 @@ import XCTest class TestSubject: XCTestCase { func testReturnsPlatformContextIfEnabled() { let subject = Subject(platformContext: true, geoLocationContext: false) - let platformDict = subject.platformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + let platformDict = subject.platformDict(userAnonymisation: false) XCTAssertNotNil(platformDict) XCTAssertNotNil(platformDict?.dictionary[kSPPlatformOsType]) } func testDoesntReturnPlatformContextIfDisabled() { let subject = Subject(platformContext: false, geoLocationContext: false) - let platformDict = subject.platformDict(userAnonymisation: false, advertisingIdentifierRetriever: nil) + let platformDict = subject.platformDict(userAnonymisation: false) XCTAssertNil(platformDict) } diff --git a/Tests/Utils/MockDeviceInfoMonitor.swift b/Tests/Utils/MockDeviceInfoMonitor.swift index 4ff31912b..025af1e69 100644 --- a/Tests/Utils/MockDeviceInfoMonitor.swift +++ b/Tests/Utils/MockDeviceInfoMonitor.swift @@ -23,22 +23,22 @@ class MockDeviceInfoMonitor: DeviceInfoMonitor { return customAppleIdfv } - override var deviceVendor: String? { + override var deviceVendor: String { increaseMethodAccessCount("deviceVendor") return "Apple Inc." } - override var deviceModel: String? { + override var deviceModel: String { increaseMethodAccessCount("deviceModel") return "deviceModel" } - override var osVersion: String? { + override var osVersion: String { increaseMethodAccessCount("osVersion") return "13.0.0" } - override var osType: String? { + override var osType: String { increaseMethodAccessCount("osType") return "ios" }