diff options
Diffstat (limited to 'platform/ios/src/MGLMapboxEvents.m')
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.m | 831 |
1 files changed, 101 insertions, 730 deletions
diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m index 273af5b3bc..05f291d8a0 100644 --- a/platform/ios/src/MGLMapboxEvents.m +++ b/platform/ios/src/MGLMapboxEvents.m @@ -1,646 +1,166 @@ #import "MGLMapboxEvents.h" -#import <UIKit/UIKit.h> -#import <CoreLocation/CoreLocation.h> -#import "MGLAccountManager.h" +#import "NSBundle+MGLAdditions.h" #import "NSProcessInfo+MGLAdditions.h" -#import "NSException+MGLAdditions.h" -#import "MGLAPIClient.h" -#import "MGLLocationManager.h" -#import "MGLTelemetryConfig.h" -#include <mbgl/storage/reachability.h> -#include <sys/sysctl.h> +static NSString * const MGLAPIClientUserAgentBase = @"mapbox-maps-ios"; +static NSString * const MGLMapboxAccountType = @"MGLMapboxAccountType"; +static NSString * const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled"; +static NSString * const MGLMapboxMetricsDebugLoggingEnabled = @"MGLMapboxMetricsDebugLoggingEnabled"; +static NSString * const MGLTelemetryAccessToken = @"MGLTelemetryAccessToken"; +static NSString * const MGLTelemetryBaseURL = @"MGLTelemetryBaseURL"; -// Event types -NSString *const MGLEventTypeAppUserTurnstile = @"appUserTurnstile"; -NSString *const MGLEventTypeMapLoad = @"map.load"; -NSString *const MGLEventTypeMapTap = @"map.click"; -NSString *const MGLEventTypeMapDragEnd = @"map.dragend"; -NSString *const MGLEventTypeLocation = @"location"; -NSString *const MGLEventTypeLocalDebug = @"debug"; +@interface MGLMapboxEvents () -// Gestures -NSString *const MGLEventGestureSingleTap = @"SingleTap"; -NSString *const MGLEventGestureDoubleTap = @"DoubleTap"; -NSString *const MGLEventGestureTwoFingerSingleTap = @"TwoFingerTap"; -NSString *const MGLEventGestureQuickZoom = @"QuickZoom"; -NSString *const MGLEventGesturePanStart = @"Pan"; -NSString *const MGLEventGesturePinchStart = @"Pinch"; -NSString *const MGLEventGestureRotateStart = @"Rotation"; -NSString *const MGLEventGesturePitchStart = @"Pitch"; - -// Event keys -NSString *const MGLEventKeyLatitude = @"lat"; -NSString *const MGLEventKeyLongitude = @"lng"; -NSString *const MGLEventKeyZoomLevel = @"zoom"; -NSString *const MGLEventKeySpeed = @"speed"; -NSString *const MGLEventKeyCourse = @"course"; -NSString *const MGLEventKeyGestureID = @"gesture"; -NSString *const MGLEventHorizontalAccuracy = @"horizontalAccuracy"; -NSString *const MGLEventKeyLocalDebugDescription = @"debug.description"; - -static NSString *const MGLEventKeyEvent = @"event"; -static NSString *const MGLEventKeyCreated = @"created"; -static NSString *const MGLEventKeyVendorID = @"userId"; -static NSString *const MGLEventKeyModel = @"model"; -static NSString *const MGLEventKeyEnabledTelemetry = @"enabled.telemetry"; -static NSString *const MGLEventKeyOperatingSystem = @"operatingSystem"; -static NSString *const MGLEventKeyResolution = @"resolution"; -static NSString *const MGLEventKeyAccessibilityFontScale = @"accessibilityFontScale"; -static NSString *const MGLEventKeyOrientation = @"orientation"; -static NSString *const MGLEventKeyPluggedIn = @"pluggedIn"; -static NSString *const MGLEventKeyWifi = @"wifi"; -static NSString *const MGLEventKeySource = @"source"; -static NSString *const MGLEventKeySessionId = @"sessionId"; -static NSString *const MGLEventKeyApplicationState = @"applicationState"; -static NSString *const MGLEventKeyAltitude = @"altitude"; - -static NSString *const MGLMapboxAccountType = @"MGLMapboxAccountType"; -static NSString *const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled"; - -// SDK event source -static NSString *const MGLEventSource = @"mapbox"; - -// Event application state -static NSString *const MGLApplicationStateForeground = @"Foreground"; -static NSString *const MGLApplicationStateBackground = @"Background"; -static NSString *const MGLApplicationStateInactive = @"Inactive"; -static NSString *const MGLApplicationStateUnknown = @"Unknown"; - -const NSUInteger MGLMaximumEventsPerFlush = 180; -const NSTimeInterval MGLFlushInterval = 180; - -@interface MGLMapboxEventsData : NSObject - -@property (nonatomic) NSString *vendorId; -@property (nonatomic) NSString *model; -@property (nonatomic) NSString *iOSVersion; -@property (nonatomic) CGFloat scale; - -@end - -@implementation MGLMapboxEventsData - -- (instancetype)init { - if (self = [super init]) { - _vendorId = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; - _model = [self sysInfoByName:"hw.machine"]; - _iOSVersion = [NSString stringWithFormat:@"%@ %@", [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion]; - if ([UIScreen instancesRespondToSelector:@selector(nativeScale)]) { - _scale = [UIScreen mainScreen].nativeScale; - } else { - _scale = [UIScreen mainScreen].scale; - } - } - return self; -} - -- (NSString *)sysInfoByName:(char *)typeSpecifier { - size_t size; - sysctlbyname(typeSpecifier, NULL, &size, NULL, 0); - - char *answer = malloc(size); - sysctlbyname(typeSpecifier, answer, &size, NULL, 0); - - NSString *results = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding]; - - free(answer); - return results; -} +@property (nonatomic) MMEEventsManager *eventsManager; +@property (nonatomic) NSURL *baseURL; +@property (nonatomic, copy) NSString *accessToken; @end +@implementation MGLMapboxEvents -@interface MGLMapboxEvents () <MGLLocationManagerDelegate> - -@property (nonatomic) MGLMapboxEventsData *data; -@property (nonatomic, copy) NSString *appBundleId; -@property (nonatomic, readonly) NSString *instanceID; -@property (nonatomic, copy) NSString *dateForDebugLogFile; -@property (nonatomic) NSDateFormatter *rfc3339DateFormatter; -@property (nonatomic) MGLAPIClient *apiClient; -@property (nonatomic) BOOL usesTestServer; -@property (nonatomic) BOOL canEnableDebugLogging; -@property (nonatomic, getter=isPaused) BOOL paused; -@property (nonatomic) NS_MUTABLE_ARRAY_OF(MGLMapboxEventAttributes *) *eventQueue; -@property (nonatomic) dispatch_queue_t serialQueue; -@property (nonatomic) dispatch_queue_t debugLogSerialQueue; -@property (nonatomic) MGLLocationManager *locationManager; -@property (nonatomic) NSTimer *timer; -@property (nonatomic) NSDate *instanceIDRotationDate; -@property (nonatomic) NSDate *nextTurnstileSendDate; -@property (nonatomic) NSNumber *currentAccountTypeValue; -@property (nonatomic) BOOL currentMetricsEnabledValue; - -@end - -@implementation MGLMapboxEvents { - NSString *_instanceID; - UIBackgroundTaskIdentifier _backgroundTaskIdentifier; -} + (void)initialize { if (self == [MGLMapboxEvents class]) { NSBundle *bundle = [NSBundle mainBundle]; NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:MGLMapboxAccountType]; - [[NSUserDefaults standardUserDefaults] registerDefaults:@{ - MGLMapboxAccountType: accountTypeNumber ?: @0, - MGLMapboxMetricsEnabled: @YES, - @"MGLMapboxMetricsDebugLoggingEnabled": @NO, - }]; + [[NSUserDefaults standardUserDefaults] registerDefaults:@{MGLMapboxAccountType: accountTypeNumber ?: @0, + MGLMapboxMetricsEnabled: @YES, + MGLMapboxMetricsDebugLoggingEnabled: @NO}]; } } -+ (BOOL)isEnabled { -#if TARGET_OS_SIMULATOR - return NO; -#else - BOOL isLowPowerModeEnabled = NO; - if ([NSProcessInfo instancesRespondToSelector:@selector(isLowPowerModeEnabled)]) { - isLowPowerModeEnabled = [[NSProcessInfo processInfo] isLowPowerModeEnabled]; ++ (nullable instancetype)sharedInstance { + if (NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent) { + return nil; } - return ([[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled] && - [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType] == 0 && - !isLowPowerModeEnabled); -#endif -} - - -- (BOOL)debugLoggingEnabled { - return (self.canEnableDebugLogging && - [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]); + + static dispatch_once_t onceToken; + static MGLMapboxEvents *_sharedInstance; + dispatch_once(&onceToken, ^{ + _sharedInstance = [[self alloc] init]; + }); + return _sharedInstance; } -- (instancetype) init { +- (instancetype)init { self = [super init]; if (self) { - [MGLTelemetryConfig.sharedConfig configurationFromKey:[[NSUserDefaults standardUserDefaults] objectForKey:MGLMapboxMetricsProfile]]; + _eventsManager = [[MMEEventsManager alloc] init]; + _eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsDebugLoggingEnabled]; + _eventsManager.accountType = [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType]; + _eventsManager.metricsEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled]; - _currentAccountTypeValue = @0; - _currentMetricsEnabledValue = YES; - - _appBundleId = [[NSBundle mainBundle] bundleIdentifier]; - _apiClient = [[MGLAPIClient alloc] init]; - - NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString]; - _serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); - - _locationManager = [[MGLLocationManager alloc] init]; - _locationManager.delegate = self; - _paused = YES; - [self resumeMetricsCollection]; - - // Events Control - _eventQueue = [[NSMutableArray alloc] init]; - - // Setup Date Format - _rfc3339DateFormatter = [[NSDateFormatter alloc] init]; - NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; - - [_rfc3339DateFormatter setLocale:enUSPOSIXLocale]; - [_rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"]; - // Clear Any System TimeZone Cache - [NSTimeZone resetSystemTimeZone]; - [_rfc3339DateFormatter setTimeZone:[NSTimeZone systemTimeZone]]; - - // Configure logging - if ([self isProbablyAppStoreBuild]) { - self.canEnableDebugLogging = NO; - - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]) { - NSLog(@"Telemetry logging is only enabled in non-app store builds."); - } - } else { - self.canEnableDebugLogging = YES; + // It is possible for the shared instance of this class to be created because of a call to + // +[MGLAccountManager load] early on in the app lifecycle of the host application. + // If user default values for access token and base URL are available, they are stored here + // on local properties so that they can be applied later once MMEEventsManager is fully initialized + // (once -[MMEEventsManager initializeWithAccessToken:userAgentBase:hostSDKVersion:] is called. + // Normally, the telem access token and base URL are not set this way. However, overriding these values + // with user defaults can be useful for testing with an alternative (test) backend system. + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { + self.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; } - - // Watch for changes to telemetry settings by the user - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidEnterBackgroundNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidBecomeActiveNotification object:nil]; - - // Watch for Low Power Mode change events - if (&NSProcessInfoPowerStateDidChangeNotification != NULL) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:NSProcessInfoPowerStateDidChangeNotification object:nil]; + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { + self.baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil]; } return self; } -// Called implicitly from any public class convenience methods. -// May return nil if this feature is disabled. -// -+ (nullable instancetype)sharedManager { - if (NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent) { - return nil; - } - static dispatch_once_t onceToken; - static MGLMapboxEvents *_sharedManager; - dispatch_once(&onceToken, ^{ - _sharedManager = [[self alloc] init]; - }); - return _sharedManager; -} - - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; - [self pauseMetricsCollection]; } -- (NSString *)instanceID { - if (self.instanceIDRotationDate && [[NSDate date] timeIntervalSinceDate:self.instanceIDRotationDate] >= 0) { - _instanceID = nil; +- (void)userDefaultsDidChange:(NSNotification *)notification { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateNonDisablingConfigurationValues]; + [self updateDisablingConfigurationValuesWithNotification:notification]; + }); +} + +- (void)updateNonDisablingConfigurationValues { + self.eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]; + + // It is possible for `MGLTelemetryAccessToken` to have been set yet `userDefaultsDidChange:` + // is called before `setupWithAccessToken:` is called. + // In that case, setting the access token here will have no effect. In practice, that's fine + // because the access token value will be resolved when `setupWithAccessToken:` is called eventually + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { + self.eventsManager.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; } - if (!_instanceID) { - _instanceID = [[NSUUID UUID] UUIDString]; - NSTimeInterval twentyFourHourTimeInterval = 24 * 3600; - self.instanceIDRotationDate = [[NSDate date] dateByAddingTimeInterval:twentyFourHourTimeInterval]; + + // It is possible for `MGLTelemetryBaseURL` to have been set yet `userDefaultsDidChange:` + // is called before setupWithAccessToken: is called. + // In that case, setting the base URL here will have no effect. In practice, that's fine + // because the base URL value will be resolved when `setupWithAccessToken:` is called eventually + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { + NSURL *baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; + self.eventsManager.baseURL = baseURL; } - return _instanceID; } -- (void)userDefaultsDidChange:(NSNotification *)notification { - +- (void)updateDisablingConfigurationValuesWithNotification:(NSNotification *)notification { // Guard against over calling pause / resume if the values this implementation actually - // cares about have not changed - + // cares about have not changed. We guard because the pause and resume method checks CoreLocation's + // authorization status and that can drag on the main thread if done too many times (e.g. if the host + // app heavily uses the user defaults API and this method is called very frequently) if ([[notification object] respondsToSelector:@selector(objectForKey:)]) { NSUserDefaults *userDefaults = [notification object]; - NSNumber *accountType = [userDefaults objectForKey:MGLMapboxAccountType]; - BOOL metricsEnabled = [[userDefaults objectForKey:MGLMapboxMetricsEnabled] boolValue]; - - if (![accountType isEqualToNumber:self.currentAccountTypeValue] || metricsEnabled != self.currentMetricsEnabledValue) { - [self pauseOrResumeMetricsCollectionIfRequired]; - self.currentAccountTypeValue = accountType; - self.currentMetricsEnabledValue = metricsEnabled; - } - } - -} - -- (void)pauseOrResumeMetricsCollectionIfRequired { - - // [CLLocationManager authorizationStatus] has been found to block in some cases so - // dispatch the call to a non-UI thread - dispatch_async(self.serialQueue, ^{ - CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + NSInteger accountType = [userDefaults integerForKey:MGLMapboxAccountType]; + BOOL metricsEnabled = [userDefaults boolForKey:MGLMapboxMetricsEnabled]; - // Checking application state must be done on the main thread for safety and - // to avoid a thread sanitizer error - dispatch_async(dispatch_get_main_queue(), ^{ - UIApplication *application = [UIApplication sharedApplication]; - UIApplicationState state = application.applicationState; - - // Prevent blue status bar when host app has `when in use` permission only and it is not in foreground - if (status == kCLAuthorizationStatusAuthorizedWhenInUse && state == UIApplicationStateBackground) { - if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) { - _backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{ - [application endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; - }]; - [self flush]; - } - [self pauseMetricsCollection]; - return; - } + if (accountType != self.eventsManager.accountType || metricsEnabled != self.eventsManager.metricsEnabled) { + self.eventsManager.accountType = accountType; + self.eventsManager.metricsEnabled = metricsEnabled; - // Toggle pause based on current pause state, user opt-out state, and low-power state. - BOOL enabled = [[self class] isEnabled]; - if (self.paused && enabled) { - [self resumeMetricsCollection]; - } else if (!self.paused && !enabled) { - [self flush]; - [self pauseMetricsCollection]; - } - }); - }); -} - -- (void)pauseMetricsCollection { - if (self.paused) { - return; - } - - self.paused = YES; - [self.timer invalidate]; - self.timer = nil; - [self.eventQueue removeAllObjects]; - self.data = nil; - - [self.locationManager stopUpdatingLocation]; -} - -- (void)resumeMetricsCollection { - if (!self.paused || ![[self class] isEnabled]) { - return; - } - - self.paused = NO; - self.data = [[MGLMapboxEventsData alloc] init]; - - [self.locationManager startUpdatingLocation]; -} - -+ (void)flush { - [[MGLMapboxEvents sharedManager] flush]; -} - -- (void)flush { - if ([MGLAccountManager accessToken] == nil) { - return; - } - - NSArray *events = [NSArray arrayWithArray:self.eventQueue]; - [self.eventQueue removeAllObjects]; - - [self postEvents:events]; - - if (self.timer) { - [self.timer invalidate]; - self.timer = nil; - } - - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"flush"}]; -} - -- (void)pushTurnstileEvent { - if (self.nextTurnstileSendDate && [[NSDate date] timeIntervalSinceDate:self.nextTurnstileSendDate] < 0) { - return; - } - - NSString *vendorID = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; - if (!vendorID) { - return; - } - - NSDictionary *turnstileEventAttributes = @{MGLEventKeyEvent: MGLEventTypeAppUserTurnstile, - MGLEventKeyCreated: [self.rfc3339DateFormatter stringFromDate:[NSDate date]], - MGLEventKeyVendorID: vendorID, - MGLEventKeyEnabledTelemetry: @([[self class] isEnabled])}; - - if ([MGLAccountManager accessToken] == nil) { - return; - } - - __weak __typeof__(self) weakSelf = self; - [self.apiClient postEvent:turnstileEventAttributes completionHandler:^(NSError * _Nullable error) { - __strong __typeof__(weakSelf) strongSelf = weakSelf; - if (error) { - [strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error", - @"error": error}]; - return; + [self.eventsManager pauseOrResumeMetricsCollectionIfRequired]; } - [strongSelf writeEventToLocalDebugLog:turnstileEventAttributes]; - [strongSelf updateNextTurnstileSendDate]; - }]; -} - -- (void)updateNextTurnstileSendDate { - // Find the time a day from now (sometime tomorrow) - NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; - NSDateComponents *dayComponent = [[NSDateComponents alloc] init]; - dayComponent.day = 1; - NSDate *sometimeTomorrow = [calendar dateByAddingComponents:dayComponent toDate:[NSDate date] options:0]; - - // Find the start of tomorrow and use that as the next turnstile send date. The effect of this is that - // turnstile events can be sent as much as once per calendar day and always at the start of a session - // when a map load happens. - NSDate *startOfTomorrow = nil; - [calendar rangeOfUnit:NSCalendarUnitDay startDate:&startOfTomorrow interval:nil forDate:sometimeTomorrow]; - self.nextTurnstileSendDate = startOfTomorrow; -} - -+ (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - [[MGLMapboxEvents sharedManager] pushEvent:event withAttributes:attributeDictionary]; -} - -- (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - if (!event) { - return; - } - - if ([event isEqualToString:MGLEventTypeMapLoad]) { - [self pushTurnstileEvent]; - } - - if (self.paused) { - return; - } - - MGLMapboxEventAttributes *fullyFormedEvent = [self fullyFormedEventForEvent:event withAttributes:attributeDictionary]; - if (fullyFormedEvent) { - [self.eventQueue addObject:fullyFormedEvent]; - [self writeEventToLocalDebugLog:fullyFormedEvent]; - // Has Flush Limit Been Reached? - if (self.eventQueue.count >= MGLMaximumEventsPerFlush) { - [self flush]; - } else if (self.eventQueue.count == 1) { - // If this is first new event on queue start timer, - [self startTimer]; - } - } else { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Unknown event", - @"eventName": event, - @"event.attributes": attributeDictionary}]; - } -} - -#pragma mark Events - -- (MGLMapboxEventAttributes *)fullyFormedEventForEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - if ([event isEqualToString:MGLEventTypeMapLoad]) { - return [self mapLoadEventWithAttributes:attributeDictionary]; - } else if ([event isEqualToString:MGLEventTypeMapTap]) { - return [self mapClickEventWithAttributes:attributeDictionary]; - } else if ([event isEqualToString:MGLEventTypeMapDragEnd]) { - return [self mapDragEndEventWithAttributes:attributeDictionary]; - } else if ([event isEqualToString:MGLEventTypeLocation]) { - return [self locationEventWithAttributes:attributeDictionary]; } - return nil; -} - -- (MGLMapboxEventAttributes *)locationEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyEvent] = MGLEventTypeLocation; - attributes[MGLEventKeySource] = MGLEventSource; - attributes[MGLEventKeySessionId] = self.instanceID; - attributes[MGLEventKeyOperatingSystem] = self.data.iOSVersion; - NSString *currentApplicationState = [self applicationState]; - if (![currentApplicationState isEqualToString:MGLApplicationStateUnknown]) { - attributes[MGLEventKeyApplicationState] = currentApplicationState; - } - - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; -} - -- (MGLMapboxEventAttributes *)mapLoadEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyEvent] = MGLEventTypeMapLoad; - attributes[MGLEventKeyCreated] = [self.rfc3339DateFormatter stringFromDate:[NSDate date]]; - attributes[MGLEventKeyVendorID] = self.data.vendorId; - attributes[MGLEventKeyModel] = self.data.model; - attributes[MGLEventKeyOperatingSystem] = self.data.iOSVersion; - attributes[MGLEventKeyResolution] = @(self.data.scale); - attributes[MGLEventKeyAccessibilityFontScale] = @([self contentSizeScale]); - attributes[MGLEventKeyOrientation] = [self deviceOrientation]; - attributes[MGLEventKeyWifi] = @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi]); - - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; -} - -- (MGLMapboxEventAttributes *)mapClickEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [self interactionEvent]; - attributes[MGLEventKeyEvent] = MGLEventTypeMapTap; - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; -} - -- (MGLMapboxEventAttributes *)mapDragEndEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [self interactionEvent]; - attributes[MGLEventKeyEvent] = MGLEventTypeMapDragEnd; - - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; } -- (MGLMutableMapboxEventAttributes *)interactionEvent { - MGLMutableMapboxEventAttributes *attributes = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyCreated] = [self.rfc3339DateFormatter stringFromDate:[NSDate date]]; - attributes[MGLEventKeyOrientation] = [self deviceOrientation]; - attributes[MGLEventKeyWifi] = @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi]); - - return attributes; -} - -- (MGLMapboxEventAttributes *)eventForAttributes:(MGLMutableMapboxEventAttributes *)attributes attributeDictionary:(MGLMapboxEventAttributes *)attributeDictionary { - [attributes addEntriesFromDictionary:attributeDictionary]; - - return [attributes copy]; -} - -// Called implicitly from public use of +flush. -// -- (void)postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { - if (self.paused) { - return; ++ (void)setupWithAccessToken:(NSString *)accessToken { + NSString *semanticVersion = [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"]; + NSString *shortVersion = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"]; + NSString *sdkVersion = semanticVersion ?: shortVersion; + + // It is possible that an alternative access token was already set on this instance when the class was loaded + // Use it if it exists + NSString *resolvedAccessToken = [MGLMapboxEvents sharedInstance].accessToken ?: accessToken; + + [[[self sharedInstance] eventsManager] initializeWithAccessToken:resolvedAccessToken userAgentBase:MGLAPIClientUserAgentBase hostSDKVersion:sdkVersion]; + + // It is possible that an alternative base URL was set on this instance when the class was loaded + // Use it if it exists + if ([MGLMapboxEvents sharedInstance].baseURL) { + [[MGLMapboxEvents sharedInstance] eventsManager].baseURL = [MGLMapboxEvents sharedInstance].baseURL; } - - __weak __typeof__(self) weakSelf = self; - dispatch_async(self.serialQueue, ^{ - __strong __typeof__(weakSelf) strongSelf = weakSelf; - [self.apiClient postEvents:events completionHandler:^(NSError * _Nullable error) { - if (error) { - [strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error", - @"error": error}]; - } else { - [strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"post", - @"debug.eventsCount": @(events.count)}]; - } - [[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; - }]; - }); -} - -- (void)startTimer { - [self.timer invalidate]; - self.timer = [NSTimer scheduledTimerWithTimeInterval:MGLFlushInterval - target:self - selector:@selector(flush) - userInfo:nil - repeats:YES]; } -- (NSString *)deviceOrientation { - NSString *result; - - switch ([UIDevice currentDevice].orientation) { - case UIDeviceOrientationUnknown: - result = @"Unknown"; - break; - case UIDeviceOrientationPortrait: - result = @"Portrait"; - break; - case UIDeviceOrientationPortraitUpsideDown: - result = @"PortraitUpsideDown"; - break; - case UIDeviceOrientationLandscapeLeft: - result = @"LandscapeLeft"; - break; - case UIDeviceOrientationLandscapeRight: - result = @"LandscapeRight"; - break; - case UIDeviceOrientationFaceUp: - result = @"FaceUp"; - break; - case UIDeviceOrientationFaceDown: - result = @"FaceDown"; - break; - default: - result = @"Default - Unknown"; - break; - } - - return result; ++ (void)pushTurnstileEvent { + [[[self sharedInstance] eventsManager] sendTurnstileEvent]; } -- (NSString *)applicationState { - switch ([UIApplication sharedApplication].applicationState) { - case UIApplicationStateActive: - return MGLApplicationStateForeground; - case UIApplicationStateInactive: - return MGLApplicationStateInactive; - case UIApplicationStateBackground: - return MGLApplicationStateBackground; - default: - return MGLApplicationStateUnknown; - } ++ (void)pushEvent:(NSString *)event withAttributes:(MMEMapboxEventAttributes *)attributeDictionary { + [[[self sharedInstance] eventsManager] enqueueEventWithName:event attributes:attributeDictionary]; } -- (NSInteger)contentSizeScale { - NSInteger result = -9999; - - NSString *sc = [UIApplication sharedApplication].preferredContentSizeCategory; - - if ([sc isEqualToString:UIContentSizeCategoryExtraSmall]) { - result = -3; - } else if ([sc isEqualToString:UIContentSizeCategorySmall]) { - result = -2; - } else if ([sc isEqualToString:UIContentSizeCategoryMedium]) { - result = -1; - } else if ([sc isEqualToString:UIContentSizeCategoryLarge]) { - result = 0; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraLarge]) { - result = 1; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { - result = 2; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { - result = 3; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { - result = -11; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { - result = 10; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { - result = 11; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { - result = 12; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { - result = 13; - } - - return result; ++ (void)flush { + [[[self sharedInstance] eventsManager] flush]; } + (void)ensureMetricsOptoutExists { NSNumber *shownInAppNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxMetricsEnabledSettingShownInApp"]; BOOL metricsEnabledSettingShownInAppFlag = [shownInAppNumber boolValue]; - + if (!metricsEnabledSettingShownInAppFlag && [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType] == 0) { // Opt-out is not configured in UI, so check for Settings.bundle id defaultEnabledValue; NSString *appSettingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"]; - + if (appSettingsBundle) { // Dynamic Settings.bundle loading based on http://stackoverflow.com/a/510329/2094275 NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[appSettingsBundle stringByAppendingPathComponent:@"Root.plist"]]; @@ -651,7 +171,7 @@ const NSTimeInterval MGLFlushInterval = 180; } } } - + if (!defaultEnabledValue) { [NSException raise:@"Telemetry opt-out missing" format: @"End users must be able to opt out of Mapbox Telemetry in your app, either inside Settings (via Settings.bundle) or inside this app. " @@ -663,153 +183,4 @@ const NSTimeInterval MGLFlushInterval = 180; } } -#pragma mark CLLocationManagerUtilityDelegate - -- (void)locationManager:(MGLLocationManager *)locationManager didUpdateLocations:(NSArray *)locations { - for (CLLocation *loc in locations) { - double accuracy = 10000000; - double lat = floor(loc.coordinate.latitude * accuracy) / accuracy; - double lng = floor(loc.coordinate.longitude * accuracy) / accuracy; - double horizontalAccuracy = round(loc.horizontalAccuracy); - NSString *formattedDate = [self.rfc3339DateFormatter stringFromDate:loc.timestamp]; - [MGLMapboxEvents pushEvent:MGLEventTypeLocation withAttributes:@{MGLEventKeyCreated: formattedDate, - MGLEventKeyLatitude: @(lat), - MGLEventKeyLongitude: @(lng), - MGLEventKeyAltitude: @(round(loc.altitude)), - MGLEventHorizontalAccuracy: @(horizontalAccuracy)}]; - } -} - -- (void)locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerAutoPause"}]; -} - -- (void)locationManagerBackgroundLocationUpdatesDidTimeout:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerTimeout"}]; -} - -- (void)locationManagerDidStartLocationUpdates:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerStartUpdates"}]; -} - -- (void)locationManagerDidStopLocationUpdates:(MGLLocationManager *)locationManager { - [self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"locationManager.locationManagerStopUpdates"}]; -} - -#pragma mark MGLMapboxEvents Debug - -- (void)pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - if (![self debugLoggingEnabled]) { - return; - } - - if (!event) { - return; - } - - MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:attributeDictionary]; - [evt setObject:event forKey:@"event"]; - [evt setObject:[self.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"]; - [evt setValue:[self applicationState] forKey:@"applicationState"]; - [evt setValue:@([[self class] isEnabled]) forKey:@"telemetryEnabled"]; - [evt setObject:self.instanceID forKey:@"instance"]; - - MGLMapboxEventAttributes *finalEvent = [NSDictionary dictionaryWithDictionary:evt]; - [self writeEventToLocalDebugLog:finalEvent]; -} - -- (void)writeEventToLocalDebugLog:(MGLMapboxEventAttributes *)event { - if (![self debugLoggingEnabled]) { - return; - } - - NSLog(@"%@", [self stringForDebugEvent:event]); - - if (!self.dateForDebugLogFile) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd"]; - [dateFormatter setTimeZone:[NSTimeZone systemTimeZone]]; - self.dateForDebugLogFile = [dateFormatter stringFromDate:[NSDate date]]; - } - - if (!self.debugLogSerialQueue) { - NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString]; - self.debugLogSerialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.debugLog", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); - } - - dispatch_async(self.debugLogSerialQueue, ^{ - if ([NSJSONSerialization isValidJSONObject:event]) { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:nil]; - - NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - jsonString = [jsonString stringByAppendingString:@",\n"]; - - NSString *logFilePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"telemetry_log-%@.json", self.dateForDebugLogFile]]; - - NSFileManager *fileManager = [[NSFileManager alloc] init]; - if ([fileManager fileExistsAtPath:logFilePath]) { - NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; - [fileHandle seekToEndOfFile]; - [fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]]; - } else { - [fileManager createFileAtPath:logFilePath contents:[jsonString dataUsingEncoding:NSUTF8StringEncoding] attributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }]; - } - } - }); -} - -- (NSString *)stringForDebugEvent:(MGLMapboxEventAttributes *)event { - // redact potentially sensitive location details from system console log - if ([event[@"event"] isEqualToString:MGLEventTypeLocation]) { - MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:event]; - [evt setObject:@"<redacted>" forKey:@"lat"]; - [evt setObject:@"<redacted>" forKey:@"lng"]; - event = evt; - } - - return [NSString stringWithFormat:@"Mapbox Telemetry event %@", event]; -} - -- (BOOL)isProbablyAppStoreBuild { -#if TARGET_IPHONE_SIMULATOR - return NO; -#else - // BugshotKit by Marco Arment https://github.com/marcoarment/BugshotKit/ - // Adapted from https://github.com/blindsightcorp/BSMobileProvision - - NSString *binaryMobileProvision = [NSString stringWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"embedded" ofType:@"mobileprovision"] encoding:NSISOLatin1StringEncoding error:NULL]; - if (!binaryMobileProvision) { - return YES; // no provision - } - - NSScanner *scanner = [NSScanner scannerWithString:binaryMobileProvision]; - NSString *plistString; - if (![scanner scanUpToString:@"<plist" intoString:nil] || ! [scanner scanUpToString:@"</plist>" intoString:&plistString]) { - return YES; // no XML plist found in provision - } - plistString = [plistString stringByAppendingString:@"</plist>"]; - - NSData *plistdata_latin1 = [plistString dataUsingEncoding:NSISOLatin1StringEncoding]; - NSError *error = nil; - NSDictionary *mobileProvision = [NSPropertyListSerialization propertyListWithData:plistdata_latin1 options:NSPropertyListImmutable format:NULL error:&error]; - if (error) { - return YES; // unknown plist format - } - - if (!mobileProvision || ! mobileProvision.count) { - return YES; // no entitlements - } - - if (mobileProvision[@"ProvisionsAllDevices"]) { - return NO; // enterprise provisioning - } - - if (mobileProvision[@"ProvisionedDevices"] && [mobileProvision[@"ProvisionedDevices"] count]) { - return NO; // development or ad-hoc - } - - return YES; // expected development/enterprise/ad-hoc entitlements not found -#endif -} - @end |