diff options
author | John Firebaugh <john.firebaugh@gmail.com> | 2015-12-16 16:49:29 -0800 |
---|---|---|
committer | John Firebaugh <john.firebaugh@gmail.com> | 2015-12-17 10:16:22 -0800 |
commit | a7e4e6bb1c738f20f8585249fe3ab61cbbf85f71 (patch) | |
tree | 3dad4e104afa95af55860cbbafac6adc8d801048 /platform/ios/src/MGLMapboxEvents.m | |
parent | 7a4e7376e1422964cb7a456f772104e28003f4d2 (diff) | |
download | qtlocation-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.m | 880 |
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 |