summaryrefslogtreecommitdiff
path: root/platform/ios/src/MGLMapboxEvents.m
diff options
context:
space:
mode:
authorJohn Firebaugh <john.firebaugh@gmail.com>2015-12-16 16:49:29 -0800
committerJohn Firebaugh <john.firebaugh@gmail.com>2015-12-17 10:16:22 -0800
commita7e4e6bb1c738f20f8585249fe3ab61cbbf85f71 (patch)
tree3dad4e104afa95af55860cbbafac6adc8d801048 /platform/ios/src/MGLMapboxEvents.m
parent7a4e7376e1422964cb7a456f772104e28003f4d2 (diff)
downloadqtlocation-mapboxgl-a7e4e6bb1c738f20f8585249fe3ab61cbbf85f71.tar.gz
[ios] Move source to src folder, matching osx
Diffstat (limited to 'platform/ios/src/MGLMapboxEvents.m')
-rw-r--r--platform/ios/src/MGLMapboxEvents.m880
1 files changed, 880 insertions, 0 deletions
diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m
new file mode 100644
index 0000000000..259cfb3def
--- /dev/null
+++ b/platform/ios/src/MGLMapboxEvents.m
@@ -0,0 +1,880 @@
+#import "MGLMapboxEvents.h"
+
+#import <UIKit/UIKit.h>
+#import <CoreLocation/CoreLocation.h>
+
+#include <mbgl/platform/darwin/reachability.h>
+
+#import "MGLAccountManager.h"
+#import "NSProcessInfo+MGLAdditions.h"
+#import "NSBundle+MGLAdditions.h"
+#import "NSException+MGLAdditions.h"
+
+#include <sys/sysctl.h>
+
+static const NSUInteger version = 1;
+static NSString *const MGLMapboxEventsUserAgent = @"MapboxEventsiOS/1.1";
+static NSString *MGLMapboxEventsAPIBase = @"https://api.tiles.mapbox.com";
+
+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 MGLEventTypeVisit = @"visit";
+
+NSString *const MGLEventKeyLatitude = @"lat";
+NSString *const MGLEventKeyLongitude = @"lng";
+NSString *const MGLEventKeyZoomLevel = @"zoom";
+NSString *const MGLEventKeySpeed = @"speed";
+NSString *const MGLEventKeyCourse = @"course";
+NSString *const MGLEventKeyAltitude = @"altitude";
+NSString *const MGLEventKeyHorizontalAccuracy = @"horizontalAccuracy";
+NSString *const MGLEventKeyVerticalAccuracy = @"verticalAccuracy";
+NSString *const MGLEventKeyPushEnabled = @"enabled.push";
+NSString *const MGLEventKeyEmailEnabled = @"enabled.email";
+NSString *const MGLEventKeyGestureID = @"gesture";
+NSString *const MGLEventKeyArrivalDate = @"arrivalDate";
+NSString *const MGLEventKeyDepartureDate = @"departureDate";
+
+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";
+
+const NSUInteger MGLMaximumEventsPerFlush = 20;
+const NSTimeInterval MGLFlushInterval = 60;
+
+@interface MGLMapboxEventsData : NSObject
+
+// All of the following properties are written to only from
+// the main thread, but can be read on any thread.
+//
+@property (atomic) NSString *vendorId;
+@property (atomic) NSString *model;
+@property (atomic) NSString *iOSVersion;
+@property (atomic) NSString *carrier;
+@property (atomic) 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;
+}
+
+// Can be called from any thread.
+//
+- (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;
+}
+
+@end
+
+//
+// Threadsafety conventions:
+//
+// All variables accessed from more than one thread are
+// designated `atomic` and accessed through dot syntax. The
+// main thread uses underscore syntax during the
+// initialization of the variable.
+//
+// All variables accessed outside of initialization and
+// from within a single thread use underscore syntax.
+//
+// All captures of `self` from within asynchronous
+// dispatches will use a `strongSelf` to avoid cyclical
+// strong references.
+//
+
+@interface MGLMapboxEvents () <CLLocationManagerDelegate>
+
+// All of the following properties are written to only from
+// the main thread, but can be read on any thread.
+//
+@property (atomic) MGLMapboxEventsData *data;
+@property (atomic) NSString *appBundleId;
+@property (atomic) NSString *appName;
+@property (atomic) NSString *appVersion;
+@property (atomic) NSString *appBuildNumber;
+@property (atomic) NSString *instanceID;
+@property (atomic) NSDateFormatter *rfc3339DateFormatter;
+@property (atomic) NSURLSession *session;
+@property (atomic) NSData *digicertCert;
+@property (atomic) NSData *geoTrustCert;
+@property (atomic) NSData *testServerCert;
+@property (atomic) BOOL usesTestServer;
+
+// Main thread only
+@property (nonatomic) CLLocationManager *locationManager;
+
+// The paused state tracker is only ever accessed from the main thread.
+//
+@property (nonatomic, getter=isPaused) BOOL paused;
+
+// The timer is only ever accessed from the main thread.
+//
+@property (nonatomic) NSTimer *timer;
+
+// This is an array of events to push. All access to it will be
+// from our own serial queue.
+//
+@property (nonatomic) NS_MUTABLE_ARRAY_OF(MGLMapboxEventAttributes *) *eventQueue;
+
+// This is a custom serial queue for accessing the event queue.
+//
+@property (nonatomic) dispatch_queue_t serialQueue;
+
+@end
+
+@implementation MGLMapboxEvents {
+ id _userDefaultsObserver;
+}
+
++ (void)initialize {
+ if (self == [MGLMapboxEvents class]) {
+ NSBundle *bundle = [NSBundle mainBundle];
+ NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:@"MGLMapboxAccountType"];
+ [[NSUserDefaults standardUserDefaults] registerDefaults:@{
+ @"MGLMapboxAccountType": accountTypeNumber ? accountTypeNumber : @0,
+ @"MGLMapboxMetricsEnabled": @YES,
+ }];
+ }
+}
+
++ (BOOL)isEnabled {
+ return ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsEnabled"] &&
+ [[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0);
+}
+
+// Must be called from the main thread. Only called internally.
+//
+- (instancetype) init {
+ MGLAssertIsMainThread();
+
+ self = [super init];
+ if (self) {
+ if (! [MGLAccountManager mapboxMetricsEnabledSettingShownInApp] &&
+ [[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0) {
+ // Opt Out is not configured in UI, so check for Settings.bundle
+ // Put Settings bundle into memory
+ id defaultEnabledValue;
+ NSString *appSettingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
+
+ if (appSettingsBundle) {
+ // Dynamic Settings.bundle loading based on:
+ // http://stackoverflow.com/questions/510216/can-you-make-the-settings-in-settings-bundle-default-even-if-you-dont-open-the
+ NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[appSettingsBundle stringByAppendingPathComponent:@"Root.plist"]];
+ NSArray *preferences = settings[@"PreferenceSpecifiers"];
+ for (NSDictionary *prefSpecification in preferences) {
+ if ([prefSpecification[@"Key"] isEqualToString:@"MGLMapboxMetricsEnabled"]) {
+ defaultEnabledValue = prefSpecification[@"DefaultValue"];
+ }
+ }
+ }
+
+ if (!defaultEnabledValue)
+ {
+ [NSException raise:@"MGLMapboxMetricsEnabled setting missing" format:
+ @"End users must be able to opt out of Metrics in your app, either inside Settings (via Settings.bundle) or inside this app. "
+ @"If you implement the opt-out control inside this app, disable this assertion by setting MGLMapboxMetricsEnabledSettingShownInApp to YES in Info.plist."];
+ }
+ [[NSUserDefaults standardUserDefaults] registerDefaults:@{
+ @"MGLMapboxMetricsEnabled": defaultEnabledValue,
+ }];
+ }
+
+ _appBundleId = [[NSBundle mainBundle] bundleIdentifier];
+ _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
+ _appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+ _appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
+ _instanceID = [[NSUUID UUID] UUIDString];
+
+ NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
+ _serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
+
+ // Configure Events Infrastructure
+ // ===============================
+
+ // Check for TEST Metrics URL
+ NSString *testURL = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMetricsTestServerURL"];
+ if (testURL != nil) {
+ MGLMapboxEventsAPIBase = testURL;
+ _usesTestServer = YES;
+ } else {
+ // Explicitly Set For Clarity
+ _usesTestServer = NO;
+ }
+
+ _paused = YES;
+ [self resumeMetricsCollection];
+ NSBundle *resourceBundle = [NSBundle bundleWithPath:[NSBundle mgl_resourceBundlePath]];
+
+ // Load Local Copy of Server's Public Key
+ NSString *cerPath = nil;
+ cerPath = [resourceBundle pathForResource:@"api_mapbox_com-geotrust" ofType:@"der"];
+ if (cerPath != nil) {
+ _geoTrustCert = [NSData dataWithContentsOfFile:cerPath];
+ }
+
+ cerPath = [resourceBundle pathForResource:@"api_mapbox_com-digicert" ofType:@"der"];
+ if (cerPath != nil) {
+ _digicertCert = [NSData dataWithContentsOfFile:cerPath];
+ }
+ cerPath = [resourceBundle pathForResource:@"star_tilestream_net" ofType:@"der"];
+ if (cerPath != nil) {
+ _testServerCert = [NSData dataWithContentsOfFile:cerPath];
+ }
+
+ // 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]];
+
+ // Enable Battery Monitoring
+ [UIDevice currentDevice].batteryMonitoringEnabled = YES;
+
+ __weak MGLMapboxEvents *weakSelf = self;
+ _userDefaultsObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification
+ object:nil
+ queue:[NSOperationQueue mainQueue]
+ usingBlock:
+ ^(NSNotification *notification) {
+ MGLMapboxEvents *strongSelf = weakSelf;
+ [strongSelf validate];
+ }];
+
+ // Turn the Mapbox Turnstile to Count App Users
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ [self pushTurnstileEvent];
+ });
+ }
+ return self;
+}
+
+// Can be called from any thread. 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;
+ void (^setupBlock)() = ^{
+ dispatch_once(&onceToken, ^{
+ _sharedManager = [[self alloc] init];
+ });
+ };
+ if ( ! [[NSThread currentThread] isMainThread]) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ setupBlock();
+ });
+ }
+ else {
+ setupBlock();
+ }
+ return _sharedManager;
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:_userDefaultsObserver];
+ [self pauseMetricsCollection];
+}
+
++ (void)validate {
+ [[MGLMapboxEvents sharedManager] validate];
+}
+
+// Used to determine if Mapbox Metrics should be collected at any given point in time
+- (void)validate {
+ MGLAssertIsMainThread();
+ BOOL enabledInSettings = [[self class] isEnabled];
+ if (self.paused && enabledInSettings) {
+ [self resumeMetricsCollection];
+ } else if (!self.paused && !enabledInSettings) {
+ [self pauseMetricsCollection];
+ }
+
+ [self validateUpdatingLocation];
+}
+
+- (void)validateUpdatingLocation {
+ MGLAssertIsMainThread();
+ if (self.paused) {
+ [self stopUpdatingLocation];
+ } else {
+ CLAuthorizationStatus authStatus = [CLLocationManager authorizationStatus];
+ if (authStatus == kCLAuthorizationStatusDenied ||
+ authStatus == kCLAuthorizationStatusRestricted) {
+ [self stopUpdatingLocation];
+ } else if (authStatus == kCLAuthorizationStatusAuthorized ||
+ authStatus == kCLAuthorizationStatusAuthorizedWhenInUse) {
+ [self startUpdatingLocation];
+ }
+ }
+}
+
++ (void)pauseMetricsCollection {
+ [[MGLMapboxEvents sharedManager] pauseMetricsCollection];
+}
+
+// Must be called from the main thread.
+//
+- (void)pauseMetricsCollection {
+ MGLAssertIsMainThread();
+ if (self.paused) {
+ return;
+ }
+ self.paused = YES;
+ [_timer invalidate];
+ _timer = nil;
+ [_eventQueue removeAllObjects];
+ _data = nil;
+ [_session invalidateAndCancel];
+ _session = nil;
+
+ [self validateUpdatingLocation];
+}
+
+- (void)stopUpdatingLocation {
+ [_locationManager stopUpdatingLocation];
+
+ // -[CLLocationManager stopMonitoringVisits] is only available in iOS 8+.
+ if ([_locationManager respondsToSelector:@selector(stopMonitoringVisits)]) {
+ [_locationManager stopMonitoringVisits];
+ }
+
+ _locationManager = nil;
+}
+
++ (void)resumeMetricsCollection {
+ [[MGLMapboxEvents sharedManager] resumeMetricsCollection];
+}
+
+// Must be called from the main thread.
+//
+- (void)resumeMetricsCollection {
+ MGLAssertIsMainThread();
+ if (!self.paused || ![[self class] isEnabled]) {
+ return;
+ }
+ self.paused = NO;
+ _data = [[MGLMapboxEventsData alloc] init];
+ _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
+
+ [self validateUpdatingLocation];
+}
+
+- (void)startUpdatingLocation {
+ MGLAssertIsMainThread();
+ if (_locationManager || _paused) {
+ NSAssert(!(_locationManager && _paused),
+ @"MGLMapboxEvents should not have a CLLocationManager while paused.");
+ return;
+ }
+ _locationManager = [[CLLocationManager alloc] init];
+ _locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
+ _locationManager.distanceFilter = 10;
+ _locationManager.delegate = self;
+
+ [_locationManager startUpdatingLocation];
+
+ // -[CLLocationManager startMonitoringVisits] is only available in iOS 8+.
+ if ([_locationManager respondsToSelector:@selector(startMonitoringVisits)]) {
+ [_locationManager startMonitoringVisits];
+ }
+}
+
+- (void) pushTurnstileEvent {
+
+ __weak MGLMapboxEvents *weakSelf = self;
+
+ dispatch_async(_serialQueue, ^{
+
+ MGLMapboxEvents *strongSelf = weakSelf;
+
+ if ( ! strongSelf) return;
+
+ // Build only IDFV event
+ NSString *vid = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
+ NSDictionary *vevt = @{
+ @"event" : MGLEventTypeAppUserTurnstile,
+ @"created" : [strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]],
+ @"appBundleId" : strongSelf.appBundleId,
+ @"vendorId": vid,
+ @"version": @(version),
+ @"instance": strongSelf.instanceID
+ };
+
+ // Add to Queue
+ [_eventQueue addObject:vevt];
+
+ // Flush
+ [strongSelf flush];
+ });
+}
+
+// Can be called from any thread. Can be called rapidly from
+// the UI thread, so performance is paramount.
+//
++ (void) pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ [[MGLMapboxEvents sharedManager] pushEvent:event withAttributes:attributeDictionary];
+ });
+}
+
+// Can be called from any thread. Called implicitly from public
+// use of +pushEvent:withAttributes:.
+//
+- (void) pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
+ __weak MGLMapboxEvents *weakSelf = self;
+
+ dispatch_async(_serialQueue, ^{
+
+ MGLMapboxEvents *strongSelf = weakSelf;
+
+ if ( ! strongSelf) return;
+
+ // Metrics Collection Has Been Paused
+ if (_paused) {
+ return;
+ }
+
+ if (!event) return;
+
+ MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:attributeDictionary];
+ // mapbox-events stock attributes
+ [evt setObject:event forKey:@"event"];
+ [evt setObject:@(version) forKey:@"version"];
+ [evt setObject:[strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"];
+ [evt setObject:strongSelf.instanceID forKey:@"instance"];
+ [evt setObject:strongSelf.data.vendorId forKey:@"vendorId"];
+ [evt setObject:strongSelf.appBundleId forKeyedSubscript:@"appBundleId"];
+
+ // mapbox-events-ios stock attributes
+ [evt setValue:strongSelf.data.model forKey:@"model"];
+ [evt setValue:strongSelf.data.iOSVersion forKey:@"operatingSystem"];
+ [evt setValue:[strongSelf deviceOrientation] forKey:@"orientation"];
+ [evt setValue:@((int)(100 * [UIDevice currentDevice].batteryLevel)) forKey:@"batteryLevel"];
+ [evt setValue:@(strongSelf.data.scale) forKey:@"resolution"];
+
+ MGLReachability *reachability = [MGLReachability reachabilityForLocalWiFi];
+ [evt setValue:([reachability isReachableViaWiFi] ? @YES : @NO) forKey:@"wifi"];
+
+ [evt setValue:[strongSelf applicationState] forKey:@"applicationState"];
+
+ [evt setValue:@([strongSelf contentSizeScale]) forKey:@"accessibilityFontScale"];
+
+ // Make Immutable Version
+ NSDictionary *finalEvent = [NSDictionary dictionaryWithDictionary:evt];
+
+ // Put On The Queue
+ [_eventQueue addObject:finalEvent];
+
+ // Has Flush Limit Been Reached?
+ if (_eventQueue.count >= MGLMaximumEventsPerFlush) {
+ [strongSelf flush];
+ } else if (_eventQueue.count == 1) {
+ // If this is first new event on queue start timer,
+ [strongSelf startTimer];
+ }
+ });
+}
+
+// Can be called from any thread.
+//
++ (void) flush {
+ [[MGLMapboxEvents sharedManager] flush];
+}
+
+// Can be called from any thread.
+//
+- (void) flush {
+ if ([MGLAccountManager accessToken] == nil) return;
+
+ __weak MGLMapboxEvents *weakSelf = self;
+
+ dispatch_async(_serialQueue, ^{
+ MGLMapboxEvents *strongSelf = weakSelf;
+ if ( ! strongSelf || [_eventQueue count] == 0) return;
+
+ // Make an immutable copy
+ NSArray *events = [NSArray arrayWithArray:_eventQueue];
+
+ // Update Queue to remove events sent to server
+ [_eventQueue removeAllObjects];
+
+ // Send Array of Events to Server
+ [strongSelf postEvents:events];
+
+ // Cancel Any Timer That May Running
+ if (strongSelf.timer) {
+ [strongSelf.timer invalidate];
+ strongSelf.timer = nil;
+ }
+ });
+}
+
+// Can be called from any thread. Called implicitly from public
+// use of +flush. Posts an async network request to upload metrics.
+//
+- (void) postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events {
+ __weak MGLMapboxEvents *weakSelf = self;
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
+ MGLMapboxEvents *strongSelf = weakSelf;
+ if (!strongSelf) return;
+
+ // Setup URL Request
+ NSString *url = [NSString stringWithFormat:@"%@/events/v1?access_token=%@", MGLMapboxEventsAPIBase, [MGLAccountManager accessToken]];
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
+ [request setValue:strongSelf.userAgent forHTTPHeaderField:@"User-Agent"];
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
+ [request setHTTPMethod:@"POST"];
+
+ // Convert Array of Dictionaries to JSON
+ if ([NSJSONSerialization isValidJSONObject:events]) {
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:events options:NSJSONWritingPrettyPrinted error:nil];
+ [request setHTTPBody:jsonData];
+
+ // Send non blocking HTTP Request to server
+ [[_session dataTaskWithRequest:request] resume];
+ }
+ });
+}
+
+// Can be called from any thread.
+//
+- (void) startTimer {
+ void (^timerBlock)() = ^{
+ // Stop Timer if it already exists
+ [_timer invalidate];
+
+ // Start New Timer
+ _timer = [NSTimer scheduledTimerWithTimeInterval:MGLFlushInterval
+ target:self
+ selector:@selector(flush)
+ userInfo:nil
+ repeats:YES];
+ };
+
+ if ( ! [[NSThread currentThread] isMainThread]) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ timerBlock();
+ });
+ } else {
+ timerBlock();
+ }
+}
+
+// Can be called from any thread.
+//
+- (NSString *) userAgent {
+ return [NSString stringWithFormat:@"%@/%@/%@ %@", self.appName, self.appVersion, self.appBuildNumber, MGLMapboxEventsUserAgent];
+}
+
+// Can be called from any thread.
+//
+- (NSString *) deviceOrientation {
+ __block NSString *result;
+
+ NSString *(^deviceOrientationBlock)(void) = ^{
+ 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;
+ };
+
+ if ( ! [[NSThread currentThread] isMainThread]) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ result = deviceOrientationBlock();
+ });
+ } else {
+ result = deviceOrientationBlock();
+ }
+
+ return result;
+}
+
+// Can be called from any thread.
+//
+- (NSString *) applicationState {
+ __block NSString *result;
+
+ NSString *(^applicationStateBlock)(void) = ^{
+ switch ([UIApplication sharedApplication].applicationState) {
+ case UIApplicationStateActive:
+ result = @"Active";
+ break;
+ case UIApplicationStateInactive:
+ result = @"Inactive";
+ break;
+ case UIApplicationStateBackground:
+ result = @"Background";
+ break;
+ default:
+ result = @"Default - Unknown";
+ break;
+ }
+
+ return result;
+ };
+
+ if ( ! [[NSThread currentThread] isMainThread]) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ result = applicationStateBlock();
+ });
+ } else {
+ result = applicationStateBlock();
+ }
+
+ return result;
+}
+
+// Can be called from any thread.
+//
+- (NSInteger) contentSizeScale {
+ __block NSInteger result = -9999;
+
+ NSInteger (^contentSizeScaleBlock)(void) = ^{
+ 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;
+ };
+
+ if ( ! [[NSThread currentThread] isMainThread]) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ result = contentSizeScaleBlock();
+ });
+ } else {
+ result = contentSizeScaleBlock();
+ }
+
+ return result;
+}
+
+// Can be called from any thread.
+//
++ (BOOL) checkPushEnabled {
+ BOOL (^pushCheckBlock)(void) = ^{
+ BOOL blockResult;
+ if ([[UIApplication sharedApplication] respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
+ // iOS 8+
+ blockResult = [[UIApplication sharedApplication] isRegisteredForRemoteNotifications];
+ } else {
+ // iOS 7
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ UIRemoteNotificationType types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
+ blockResult = (types == UIRemoteNotificationTypeNone) ? NO : YES;
+#pragma clang diagnostic pop
+ }
+ return blockResult;
+ };
+
+ __block BOOL result;
+
+ if ( ! [[NSThread currentThread] isMainThread]) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ result = pushCheckBlock();
+ });
+ } else {
+ result = pushCheckBlock();
+ }
+
+ return result;
+}
+
+#pragma mark CLLocationManagerDelegate
+- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
+ // Iterate through locations to pass all data
+ for (CLLocation *loc in locations) {
+ [MGLMapboxEvents pushEvent:MGLEventTypeLocation withAttributes:@{
+ MGLEventKeyLatitude: @(loc.coordinate.latitude),
+ MGLEventKeyLongitude: @(loc.coordinate.longitude),
+ MGLEventKeySpeed: @(loc.speed),
+ MGLEventKeyCourse: @(loc.course),
+ MGLEventKeyAltitude: @(round(loc.altitude)),
+ MGLEventKeyHorizontalAccuracy: @(round(loc.horizontalAccuracy)),
+ MGLEventKeyVerticalAccuracy: @(round(loc.verticalAccuracy))
+ }];
+ }
+}
+
+- (void)locationManager:(CLLocationManager *)manager didVisit:(CLVisit *)visit {
+ [MGLMapboxEvents pushEvent:MGLEventTypeVisit withAttributes:@{
+ MGLEventKeyLatitude: @(visit.coordinate.latitude),
+ MGLEventKeyLongitude: @(visit.coordinate.longitude),
+ MGLEventKeyHorizontalAccuracy: @(round(visit.horizontalAccuracy)),
+ MGLEventKeyArrivalDate: [[NSDate distantPast] isEqualToDate:visit.arrivalDate] ? [NSNull null] : [_rfc3339DateFormatter stringFromDate:visit.arrivalDate],
+ MGLEventKeyDepartureDate: [[NSDate distantFuture] isEqualToDate:visit.departureDate] ? [NSNull null] : [_rfc3339DateFormatter stringFromDate:visit.departureDate]
+ }];
+}
+
+- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
+ [self validateUpdatingLocation];
+}
+
+#pragma mark NSURLSessionDelegate
+- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
+
+ if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+
+ SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
+ SecTrustResultType trustResult;
+
+ // Validate the certificate chain with the device's trust store anyway
+ // This *might* give use revocation checking
+ SecTrustEvaluate(serverTrust, &trustResult);
+ if (trustResult == kSecTrustResultUnspecified)
+ {
+ // Look for a pinned certificate in the server's certificate chain
+ long numKeys = SecTrustGetCertificateCount(serverTrust);
+
+ BOOL found = NO;
+ // Try GeoTrust Cert First
+ for (int lc = 0; lc < numKeys; lc++) {
+ SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc);
+ NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
+
+ // Compare Remote Key With Local Version
+ if ([remoteCertificateData isEqualToData:_geoTrustCert]) {
+ // Found the certificate; continue connecting
+ completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+ found = YES;
+ break;
+ }
+ }
+
+ if (!found) {
+ // Fallback to Digicert Cert
+ for (int lc = 0; lc < numKeys; lc++) {
+ SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc);
+ NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
+
+ // Compare Remote Key With Local Version
+ if ([remoteCertificateData isEqualToData:_digicertCert]) {
+ // Found the certificate; continue connecting
+ completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+ found = YES;
+ break;
+ }
+ }
+
+ if (!found && _usesTestServer) {
+ // See if this is test server
+ for (int lc = 0; lc < numKeys; lc++) {
+ SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc);
+ NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
+
+ // Compare Remote Key With Local Version
+ if ([remoteCertificateData isEqualToData:_testServerCert]) {
+ // Found the certificate; continue connecting
+ completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+ found = YES;
+ break;
+ }
+ }
+ }
+
+ if (!found) {
+ // The certificate wasn't found in GeoTrust nor Digicert. Cancel the connection.
+ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+ }
+ }
+ }
+ else
+ {
+ // Certificate chain validation failed; cancel the connection
+ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+ }
+ }
+
+}
+
+@end