diff options
author | Justin R. Miller <incanus@codesorcery.net> | 2015-03-26 21:27:56 -0700 |
---|---|---|
committer | Justin R. Miller <incanus@codesorcery.net> | 2015-03-26 21:27:56 -0700 |
commit | 83f083c7b5854db25168240a4ad7a313f2765f05 (patch) | |
tree | 5e1251987b10e565b410024e77da164ebc143852 | |
parent | 00d2cbe5cbab789388d3b06b899d007b2c44e61a (diff) | |
download | qtlocation-mapboxgl-83f083c7b5854db25168240a4ad7a313f2765f05.tar.gz |
fixes #1116: metrics stability & performance refactor
-rw-r--r-- | include/mbgl/ios/MGLMapView.h | 4 | ||||
-rw-r--r-- | include/mbgl/ios/MGLMapboxEvents.h | 53 | ||||
-rw-r--r-- | include/mbgl/ios/MGLMetricsLocationManager.h | 21 | ||||
-rw-r--r-- | platform/ios/MGLMapView.mm | 107 | ||||
-rw-r--r-- | platform/ios/MGLMapboxEvents.m | 573 | ||||
-rw-r--r-- | platform/ios/MGLMetricsLocationManager.m | 62 |
6 files changed, 495 insertions, 325 deletions
diff --git a/include/mbgl/ios/MGLMapView.h b/include/mbgl/ios/MGLMapView.h index b41e2f5d72..aa92b0c822 100644 --- a/include/mbgl/ios/MGLMapView.h +++ b/include/mbgl/ios/MGLMapView.h @@ -1,8 +1,8 @@ +#import "MGLTypes.h" + #import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> -#import "MGLTypes.h" - @class MGLUserLocation; @protocol MGLMapViewDelegate; diff --git a/include/mbgl/ios/MGLMapboxEvents.h b/include/mbgl/ios/MGLMapboxEvents.h index bb28ec26c5..541f21df43 100644 --- a/include/mbgl/ios/MGLMapboxEvents.h +++ b/include/mbgl/ios/MGLMapboxEvents.h @@ -1,26 +1,43 @@ -// -// MapboxEvents.h -// MapboxEvents -// -// Created by Brad Leege on 3/5/15. -// Copyright (c) 2015 Mapbox. All rights reserved. -// - #import <Foundation/Foundation.h> +extern NSString *const MGLEventMapLoad; +extern NSString *const MGLEventMapTap; +extern NSString *const MGLEventMapSingleTap; +extern NSString *const MGLEventMapDoubleTap; +extern NSString *const MGLEventMapTwoFingerSingleTap; +extern NSString *const MGLEventMapQuickZoom; +extern NSString *const MGLEventMapPanStart; +extern NSString *const MGLEventMapPanEnd; +extern NSString *const MGLEventMapPinchStart; +extern NSString *const MGLEventMapRotateStart; +extern NSString *const MGLEventMapLocation; + @interface MGLMapboxEvents : NSObject -@property (atomic) NSInteger flushAt; -@property (atomic) NSInteger flushAfter; -@property (atomic) NSString *api; -@property (atomic) NSString *token; -@property (atomic) NSString *appName; -@property (atomic) NSString *appVersion; +// You must call these methods from the main thread. +// ++ (void) setToken:(NSString *)token; ++ (void) setAppName:(NSString *)appName; ++ (void) setAppVersion:(NSString *)appVersion; -+ (id)sharedManager; +// You can call this method from any thread. Significant work will +// be dispatched to a low-priority background queue and all +// resulting calls are guaranteed threadsafe. +// +// Events or attributes passed could be accessed on non-main threads, +// so you must not reference UI elements from within any arguments. +// Copy any values needed first or create dedicated methods in this +// class for threadsafe access to UIKit classes. +// ++ (void) pushEvent:(NSString *)event withAttributes:(NSDictionary *)attributeDictionary; -- (void) pushEvent:(NSString *)event withAttributes:(NSDictionary *)attributeDictionary; +// You can call these methods from any thread. +// ++ (NSString *) checkEmailEnabled; ++ (BOOL) checkPushEnabled; -- (void) flush; +// You can call this method from any thread. +// ++ (void) flush; -@end
\ No newline at end of file +@end diff --git a/include/mbgl/ios/MGLMetricsLocationManager.h b/include/mbgl/ios/MGLMetricsLocationManager.h index ce04ae9ef6..7281d05010 100644 --- a/include/mbgl/ios/MGLMetricsLocationManager.h +++ b/include/mbgl/ios/MGLMetricsLocationManager.h @@ -1,22 +1,9 @@ -// -// MBLocationManager.h -// Hermes -// -// Created by Brad Leege on 3/8/15. -// Copyright (c) 2015 Mapbox. All rights reserved. -// - #import <Foundation/Foundation.h> -#import "CoreLocation/CoreLocation.h" -@interface MGLMetricsLocationManager : NSObject <CLLocationManagerDelegate> +@interface MGLMetricsLocationManager : NSObject -+ (id)sharedManager; - -- (BOOL) isAuthorizedStatusDetermined; -- (void) requestAlwaysAuthorization; - -- (void) startUpdatingLocation; -- (void) stopUpdatingLocation; +// This method can be called from any thread. +// ++ (instancetype)sharedManager; @end diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index a4604bc580..92367a3cf2 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -22,7 +22,6 @@ #import "SMCalloutView.h" #import "MGLMapboxEvents.h" -#import "MGLMetricsLocationManager.h" #import <algorithm> @@ -155,7 +154,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; if (accessToken) { mbglMap->setAccessToken((std::string)[accessToken UTF8String]); - [[MGLMapboxEvents sharedManager] setToken:accessToken]; + [MGLMapboxEvents setToken:accessToken]; } } @@ -195,16 +194,11 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; // self.accessibilityLabel = @"Map"; - // setup Metrics - MGLMapboxEvents *events = [MGLMapboxEvents sharedManager]; + // metrics: initial setup NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - if (appName != nil) { - events.appName = appName; - } - if (appVersion != nil) { - events.appVersion = appVersion; - } + if (appName != nil) [MGLMapboxEvents setAppName:appName]; + if (appVersion != nil) [MGLMapboxEvents setAppVersion:appVersion]; // create GL view // @@ -345,9 +339,6 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; - // setup dedicated location manager for metrics - [MGLMetricsLocationManager sharedManager]; - // set initial position // mbglMap->setLatLngZoom(mbgl::LatLng(0, 0), mbglMap->getMinZoom()); @@ -360,28 +351,18 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; // start the main loop mbglMap->start(); - - // Fire map.load on a background thread - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ - - NSMutableDictionary *evt = [[NSMutableDictionary alloc] init]; - [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getLatLng().latitude] forKey:@"lat"]; - [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getLatLng().longitude] forKey:@"lng"]; - [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getZoom()] forKey:@"zoom"]; - [evt setValue:[[NSNumber alloc] initWithBool:[[UIApplication sharedApplication] isRegisteredForRemoteNotifications]] forKey:@"enabled.push"]; - - NSString *email = @"Unknown"; - Class MFMailComposeViewController = NSClassFromString(@"MFMailComposeViewController"); - if (MFMailComposeViewController) { - SEL canSendMail = NSSelectorFromString(@"canSendMail"); - BOOL sendMail = ((BOOL (*)(id, SEL))[MFMailComposeViewController methodForSelector:canSendMail])(MFMailComposeViewController, canSendMail); - email = [NSString stringWithFormat:@"%i", sendMail]; - } - [evt setValue:email forKey:@"enabled.email"]; + // metrics: map load event + const mbgl::LatLng latLng = mbglMap->getLatLng(); + const double zoom = mbglMap->getZoom(); + + [MGLMapboxEvents pushEvent:MGLEventMapLoad withAttributes:@{ + @"lat": @(latLng.latitude), + @"lng": @(latLng.longitude), + @"zoom": @(zoom), + @"enabled.push": @([MGLMapboxEvents checkPushEnabled]), + @"enabled.email": [MGLMapboxEvents checkEmailEnabled] + }]; - [[MGLMapboxEvents sharedManager] pushEvent:@"map.load" withAttributes:evt]; - }); - return YES; } @@ -566,8 +547,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)appDidBackground:(NSNotification *)notification { - // Flush Any Events Still In Queue - [[MGLMapboxEvents sharedManager] flush]; + [MGLMapboxEvents flush]; mbglMap->stop(); @@ -606,7 +586,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handlePanGesture:(UIPanGestureRecognizer *)pan { - [self trackGestureEvent:@"Pan" forRecognizer:pan]; + [self trackGestureEvent:MGLEventMapPanStart forRecognizer:pan]; if ( ! self.isScrollEnabled) return; @@ -668,21 +648,22 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)]; } - // Send Map Drag End Event - CGPoint ptInView = CGPointMake([pan locationInView:pan.view].x, [pan locationInView:pan.view].y); - CLLocationCoordinate2D coord = [self convertPoint:ptInView toCoordinateFromView:pan.view]; - NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.latitude] forKey:@"lat"]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.longitude] forKey:@"lng"]; - [dict setValue:[[NSNumber alloc] initWithDouble:[self zoomLevel]] forKey:@"zoom"]; + // metrics: pan end + CGPoint pointInView = CGPointMake([pan locationInView:pan.view].x, [pan locationInView:pan.view].y); + CLLocationCoordinate2D panCoordinate = [self convertPoint:pointInView toCoordinateFromView:pan.view]; + double zoom = [self zoomLevel]; - [[MGLMapboxEvents sharedManager] pushEvent:@"map.dragend" withAttributes:dict]; + [MGLMapboxEvents pushEvent:MGLEventMapPanEnd withAttributes:@{ + @"lat": @(panCoordinate.latitude), + @"lng": @(panCoordinate.longitude), + @"zoom": @(zoom) + }]; } } - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch { - [self trackGestureEvent:@"Pinch" forRecognizer:pinch]; + [self trackGestureEvent:MGLEventMapPinchStart forRecognizer:pinch]; if ( ! self.isZoomEnabled) return; @@ -720,7 +701,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate { - [self trackGestureEvent:@"Rotation" forRecognizer:rotate]; + [self trackGestureEvent:MGLEventMapRotateStart forRecognizer:rotate]; if ( ! self.isRotateEnabled) return; @@ -762,7 +743,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap { - [self trackGestureEvent:@"SingleTap" forRecognizer:singleTap]; + [self trackGestureEvent:MGLEventMapSingleTap forRecognizer:singleTap]; CGPoint tapPoint = [singleTap locationInView:self]; @@ -885,7 +866,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleDoubleTapGesture:(UITapGestureRecognizer *)doubleTap { - [self trackGestureEvent:@"DoubleTap" forRecognizer:doubleTap]; + [self trackGestureEvent:MGLEventMapDoubleTap forRecognizer:doubleTap]; if ( ! self.isZoomEnabled) return; @@ -916,7 +897,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleTwoFingerTapGesture:(UITapGestureRecognizer *)twoFingerTap { - [self trackGestureEvent:@"TwoFingerTap" forRecognizer:twoFingerTap]; + [self trackGestureEvent:MGLEventMapTwoFingerSingleTap forRecognizer:twoFingerTap]; if ( ! self.isZoomEnabled) return; @@ -949,7 +930,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleQuickZoomGesture:(UILongPressGestureRecognizer *)quickZoom { - [self trackGestureEvent:@"QuickZoom" forRecognizer:quickZoom]; + [self trackGestureEvent:MGLEventMapQuickZoom forRecognizer:quickZoom]; if ( ! self.isZoomEnabled) return; @@ -997,20 +978,18 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]); } -- (void)trackGestureEvent:(NSString *)gesture forRecognizer:(UIGestureRecognizer *)recognizer +- (void)trackGestureEvent:(NSString *)gestureID forRecognizer:(UIGestureRecognizer *)recognizer { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ - // Send Map Zoom Event - CGPoint ptInView = CGPointMake([recognizer locationInView:recognizer.view].x, [recognizer locationInView:recognizer.view].y); - CLLocationCoordinate2D coord = [self convertPoint:ptInView toCoordinateFromView:recognizer.view]; - NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.latitude] forKey:@"lat"]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.longitude] forKey:@"lng"]; - [dict setValue:[[NSNumber alloc] initWithDouble:[self zoomLevel]] forKey:@"zoom"]; - [dict setValue:gesture forKey:@"gesture"]; - - [[MGLMapboxEvents sharedManager] pushEvent:@"map.click" withAttributes:dict]; - }); + CGPoint pointInView = CGPointMake([recognizer locationInView:recognizer.view].x, [recognizer locationInView:recognizer.view].y); + CLLocationCoordinate2D gestureCoordinate = [self convertPoint:pointInView toCoordinateFromView:recognizer.view]; + double zoom = [self zoomLevel]; + + [MGLMapboxEvents pushEvent:MGLEventMapTap withAttributes:@{ + @"lat": @(gestureCoordinate.latitude), + @"lng": @(gestureCoordinate.longitude), + @"zoom": @(zoom), + @"gesture": gestureID + }]; } #pragma mark - Properties - diff --git a/platform/ios/MGLMapboxEvents.m b/platform/ios/MGLMapboxEvents.m index 8a037f18f5..7fded59f3e 100644 --- a/platform/ios/MGLMapboxEvents.m +++ b/platform/ios/MGLMapboxEvents.m @@ -1,44 +1,86 @@ -// -// MapboxEvents.m -// MapboxEvents -// -// Dynamic detection of ASIdentifierManager from Mixpanel -// https://github.com/mixpanel/mixpanel-iphone/blob/master/LICENSE -// -// Created by Brad Leege on 3/5/15. -// Copyright (c) 2015 Mapbox. All rights reserved. -// - #import "MGLMapboxEvents.h" + #import <UIKit/UIKit.h> +#import <SystemConfiguration/CaptiveNetwork.h> #import <CoreTelephony/CTTelephonyNetworkInfo.h> #import <CoreTelephony/CTCarrier.h> + +#import "MGLMetricsLocationManager.h" + #include <sys/sysctl.h> -#import <SystemConfiguration/CaptiveNetwork.h> + +static NSString *const MGLMapboxEventsUserAgent = @"MapboxEventsiOS/1.0"; +static NSString *const MGLMapboxEventsAPIBase = @"https://api.tiles.mapbox.com"; + +NSString *const MGLEventMapLoad = @"map.load"; +NSString *const MGLEventMapTap = @"map.click"; +NSString *const MGLEventMapSingleTap = @"SingleTap"; +NSString *const MGLEventMapDoubleTap = @"DoubleTap"; +NSString *const MGLEventMapTwoFingerSingleTap = @"TwoFingerTap"; +NSString *const MGLEventMapQuickZoom = @"QuickZoom"; +NSString *const MGLEventMapPanStart = @"Pan"; +NSString *const MGLEventMapPanEnd = @"map.dragend"; +NSString *const MGLEventMapPinchStart = @"Pinch"; +NSString *const MGLEventMapRotateStart = @"Rotation"; +NSString *const MGLEventMapLocation = @"Location"; + +// +// 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 `weakSelf` to avoid cyclical +// strong references. +// @interface MGLMapboxEvents() -@property (atomic) NSMutableArray *queue; -@property (atomic) NSString *instance; -@property (atomic) NSString *anonid; -@property (atomic) NSTimer *timer; +// All of the following properties are written to only from +// the main thread, but can be read on any thread. +// +@property (atomic) NSString *token; +@property (atomic) NSString *appName; +@property (atomic) NSString *appVersion; +@property (atomic) NSString *instanceID; +@property (atomic) NSString *anonID; @property (atomic) NSString *userAgent; -@property (atomic) dispatch_queue_t serialqPush; -@property (atomic) dispatch_queue_t serialqFlush; +@property (atomic) NSString *model; +@property (atomic) NSString *iOSVersion; +@property (atomic) NSString *carrier; +@property (atomic) NSUInteger flushAt; +@property (atomic) NSTimeInterval flushAfter; +@property (atomic) NSDateFormatter *rfc3339DateFormatter; +@property (atomic) CGFloat scale; + +// 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) NSMutableArray *eventQueue; + +// This is a custom serial queue for accessing the event queue. +// +@property (nonatomic) dispatch_queue_t serialQueue; @end @implementation MGLMapboxEvents -static MGLMapboxEvents *sharedManager = nil; - -NSDateFormatter *rfc3339DateFormatter = nil; -NSString *model; -NSString *iOSVersion; -NSString *carrier; -NSNumber *scale; +// Must be called from the main thread. Only called internally. +// +- (instancetype) init { + assert([[NSThread currentThread] isMainThread]); -- (id) init { self = [super init]; if (self) { @@ -47,6 +89,8 @@ NSNumber *scale; if(!settingsBundle) { NSLog(@"Could not find Settings.bundle"); } else { + // 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:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]]; NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"]; NSMutableDictionary *defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]]; @@ -61,16 +105,16 @@ NSNumber *scale; } NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString]; - _serialqPush = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.SERIALQPUSH", bundleID, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); - _serialqFlush = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.SERIALQFLUSH", bundleID, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); - + _serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", bundleID, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); + // Configure Events Infrastructure - _queue = [[NSMutableArray alloc] init]; + _eventQueue = [[NSMutableArray alloc] init]; _flushAt = 20; - _flushAfter = 10000; - _api = @"https://api.tiles.mapbox.com"; + _flushAfter = 60; _token = nil; - _instance = [[NSUUID UUID] UUIDString]; + _instanceID = [[NSUUID UUID] UUIDString]; + // Dynamic detection of ASIdentifierManager from Mixpanel + // https://github.com/mixpanel/mixpanel-iphone/blob/master/LICENSE Class ASIdentifierManagerClass = NSClassFromString(@"ASIdentifierManager"); if (ASIdentifierManagerClass) { SEL sharedManagerSelector = NSSelectorFromString(@"sharedManager"); @@ -81,84 +125,130 @@ NSNumber *scale; if (trackingEnabled) { SEL advertisingIdentifierSelector = NSSelectorFromString(@"advertisingIdentifier"); NSUUID *uuid = ((NSUUID* (*)(id, SEL))[sharedManager methodForSelector:advertisingIdentifierSelector])(sharedManager, advertisingIdentifierSelector); - _anonid = [uuid UUIDString]; + _anonID = [uuid UUIDString]; } else { - _anonid = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + _anonID = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; } } else { - _anonid = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + _anonID = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; } - model = [self getSysInfoByName:"hw.machine"]; - iOSVersion = [NSString stringWithFormat:@"%@ %@", [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion]; + _model = [self getSysInfoByName:"hw.machine"]; + _iOSVersion = [NSString stringWithFormat:@"%@ %@", [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion]; if ([UIScreen instancesRespondToSelector:@selector(nativeScale)]) { - scale = [[NSNumber alloc] initWithFloat:[UIScreen mainScreen].nativeScale]; + _scale = [UIScreen mainScreen].nativeScale; } else { - scale = [[NSNumber alloc] initWithFloat:[UIScreen mainScreen].scale]; + _scale = [UIScreen mainScreen].scale; } CTCarrier *carrierVendor = [[[CTTelephonyNetworkInfo alloc] init] subscriberCellularProvider]; - carrier = [carrierVendor carrierName]; + _carrier = [carrierVendor carrierName]; - _userAgent = @"MapboxEventsiOS/1.0"; + _userAgent = MGLMapboxEventsUserAgent; // Setup Date Format - rfc3339DateFormatter = [[NSDateFormatter alloc] init]; + _rfc3339DateFormatter = [[NSDateFormatter alloc] init]; NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; - [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; - [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; - [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + [_rfc3339DateFormatter setLocale:enUSPOSIXLocale]; + [_rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"]; + [_rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; } return self; } -+ (id)sharedManager { +// Can be called from any thread. Called implicitly from any +// public class convenience methods. +// ++ (instancetype)sharedManager { static dispatch_once_t onceToken; + static MGLMapboxEvents *_sharedManager; dispatch_once(&onceToken, ^{ - sharedManager = [[self alloc] init]; + void (^setupBlock)() = ^{ + _sharedManager = [[self alloc] init]; + // setup dedicated location manager on first use + [MGLMetricsLocationManager sharedManager]; + }; + if ( ! [[NSThread currentThread] isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + setupBlock(); + }); + } else { + setupBlock(); + } }); - return sharedManager; + return _sharedManager; +} + +// Must be called from the main thread. +// ++ (void) setToken:(NSString *)token { + assert([[NSThread currentThread] isMainThread]); + [MGLMapboxEvents sharedManager].token = token; } +// Must be called from the main thread. +// ++ (void) setAppName:(NSString *)appName { + assert([[NSThread currentThread] isMainThread]); + [MGLMapboxEvents sharedManager].appName = appName; +} + +// Must be called from the main thread. +// ++ (void) setAppVersion:(NSString *)appVersion { + assert([[NSThread currentThread] isMainThread]); + [MGLMapboxEvents sharedManager].appVersion = appVersion; +} + +// Can be called from any thread. Can be called rapidly from +// the UI thread, so performance is paramount. +// ++ (void) pushEvent:(NSString *)event withAttributes:(NSDictionary *)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:(NSDictionary *)attributeDictionary { - - // Opt Out Checking When Built - if (![[NSUserDefaults standardUserDefaults] boolForKey:@"mapbox_metrics_enabled_preference"]) { - [_queue removeAllObjects]; - return; - } + __weak MGLMapboxEvents *weakSelf = self; - // Add Metrics Disabled App Wide Check - if ([[NSUserDefaults standardUserDefaults] objectForKey:@"mapbox_metrics_disabled"] != nil) { - [_queue removeAllObjects]; - return; - } - - if (!event) { - return; - } - - dispatch_async(_serialqPush, ^{ + dispatch_async(_serialQueue, ^{ + // Opt Out Checking When Built + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"mapbox_metrics_enabled_preference"]) { + [_eventQueue removeAllObjects]; + return; + } + + // Add Metrics Disabled App Wide Check + if ([[NSUserDefaults standardUserDefaults] objectForKey:@"mapbox_metrics_disabled"] != nil) { + [_eventQueue removeAllObjects]; + return; + } + if (!event) return; + NSMutableDictionary *evt = [[NSMutableDictionary alloc] init]; // mapbox-events stock attributes [evt setObject:event forKey:@"event"]; - [evt setObject:[NSNumber numberWithInt:1] forKey:@"version"]; - [evt setObject:[self formatDate:[NSDate date]] forKey:@"created"]; - [evt setObject:self.instance forKey:@"instance"]; - [evt setObject:self.anonid forKey:@"anonid"]; + [evt setObject:@(1) forKey:@"version"]; + [evt setObject:[weakSelf formatDate:[NSDate date]] forKey:@"created"]; + [evt setObject:weakSelf.instanceID forKey:@"instance"]; + [evt setObject:weakSelf.anonID forKey:@"anonid"]; // mapbox-events-ios stock attributes - [evt setValue:[rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"]; - [evt setValue:model forKey:@"model"]; - [evt setValue:iOSVersion forKey:@"operatingSystem"]; - [evt setValue:[self getDeviceOrientation] forKey:@"orientation"]; - [evt setValue:[[NSNumber alloc] initWithFloat:(100 * [UIDevice currentDevice].batteryLevel)] forKey:@"batteryLevel"]; - [evt setValue:scale forKey:@"resolution"]; - [evt setValue:carrier forKey:@"carrier"]; - [evt setValue:[self getCurrentCellularNetworkConnectionType] forKey:@"cellularNetworkType"]; - [evt setValue:[self getWifiNetworkName] forKey:@"wifi"]; - [evt setValue:[NSNumber numberWithInt:[self getContentSizeScale]] forKey:@"accessibilityFontScale"]; + [evt setValue:[weakSelf.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"]; + [evt setValue:weakSelf.model forKey:@"model"]; + [evt setValue:weakSelf.iOSVersion forKey:@"operatingSystem"]; + [evt setValue:[weakSelf getDeviceOrientation] forKey:@"orientation"]; + [evt setValue:@(100 * [UIDevice currentDevice].batteryLevel) forKey:@"batteryLevel"]; + [evt setValue:@(weakSelf.scale) forKey:@"resolution"]; + [evt setValue:weakSelf.carrier forKey:@"carrier"]; + [evt setValue:[weakSelf getCurrentCellularNetworkConnectionType] forKey:@"cellularNetworkType"]; + [evt setValue:[weakSelf getWifiNetworkName] forKey:@"wifi"]; + [evt setValue:@([weakSelf getContentSizeScale]) forKey:@"accessibilityFontScale"]; for (NSString *key in [attributeDictionary allKeys]) { [evt setObject:[attributeDictionary valueForKey:key] forKey:key]; @@ -168,149 +258,220 @@ NSNumber *scale; NSDictionary *finalEvent = [NSDictionary dictionaryWithDictionary:evt]; // Put On The Queue - [self.queue addObject:finalEvent]; + [_eventQueue addObject:finalEvent]; // Has Flush Limit Been Reached? - if ((int)_queue.count >= (int)_flushAt) { - [self flush]; + if (_eventQueue.count >= weakSelf.flushAt) { + [weakSelf flush]; } // Reset Timer (Initial Starting of Timer after first event is pushed) - [self startTimer]; - + [weakSelf startTimer]; }); } +// Can be called from any thread. +// ++ (void) flush { + [[MGLMapboxEvents sharedManager] flush]; +} + +// Can be called from any thread. +// - (void) flush { - if (_token == nil) { - return; - } - - dispatch_async(_serialqFlush, ^{ - - int upper = (int)_flushAt; - if (_flushAt > [_queue count]) { - if ([_queue count] == 0) { + if (self.token == nil) return; + + __weak MGLMapboxEvents *weakSelf = self; + + dispatch_async(_serialQueue, ^{ + __block NSArray *events; + + NSUInteger upper = weakSelf.flushAt; + if (weakSelf.flushAt > [_eventQueue count]) { + if ([_eventQueue count] == 0) { return; } - upper = (int)[_queue count]; + upper = [_eventQueue count]; } // Create Array of Events to push to the Server NSRange theRange = NSMakeRange(0, upper); - NSArray *events = [_queue subarrayWithRange:theRange]; + events = [_eventQueue subarrayWithRange:theRange]; // Update Queue to remove events sent to server - [_queue removeObjectsInRange:theRange]; - + [_eventQueue removeObjectsInRange:theRange]; + // Send Array of Events to Server - [self postEvents:events]; + [weakSelf postEvents:events]; }); } +// Can be called from any thread. Called implicitly from public +// use of +flush. Posts an async network request to upload metrics. +// - (void) postEvents:(NSArray *)events { - // Setup URL Request - NSString *url = [NSString stringWithFormat:@"%@/events/v1?access_token=%@", _api, _token]; - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; - [request setValue:[self getUserAgent] 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]; + __weak MGLMapboxEvents *weakSelf = self; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + // Setup URL Request + NSString *url = [NSString stringWithFormat:@"%@/events/v1?access_token=%@", MGLMapboxEventsAPIBase, weakSelf.token]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; + [request setValue:[weakSelf getUserAgent] forHTTPHeaderField:@"User-Agent"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request setHTTPMethod:@"POST"]; - // Send non blocking HTTP Request to server - [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:nil]; - } + // 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 + [NSURLConnection sendAsynchronousRequest:request + queue:nil + completionHandler:nil]; + } + }); } +// Can be called from any thread. +// - (void) startTimer { - // Stop Timer if it already exists - if (_timer) { - [_timer invalidate]; - _timer = nil; + void (^timerBlock)() = ^{ + // Stop Timer if it already exists + if (_timer) { + [_timer invalidate]; + _timer = nil; + } + + // Start New Timer + NSTimeInterval interval = _flushAfter; + _timer = [NSTimer scheduledTimerWithTimeInterval:interval + target:self + selector:@selector(flush) + userInfo:nil repeats:YES]; + }; + + if ( ! [[NSThread currentThread] isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + timerBlock(); + }); + } else { + timerBlock(); } - - // Start New Timer - NSTimeInterval interval = (double)((NSInteger)_flushAfter); - _timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(flush) userInfo:nil repeats:YES]; } +// Can be called from any thread. +// - (NSString *) getUserAgent { - - if (_appName != nil && _appVersion != nil && ([_userAgent rangeOfString:_appName].location == NSNotFound)) { - _userAgent = [NSString stringWithFormat:@"%@/%@ %@", _appName, _appVersion, _userAgent]; + if (self.appName != nil && self.appVersion != nil && ([self.userAgent rangeOfString:self.appName].location == NSNotFound)) { + self.userAgent = [NSString stringWithFormat:@"%@/%@ %@", self.appName, self.appVersion, self.userAgent]; } - return _userAgent; + return self.userAgent; } +// Can be called from any thread. +// - (NSString *) formatDate:(NSDate *)date { - return [rfc3339DateFormatter stringFromDate:date]; + return [self.rfc3339DateFormatter stringFromDate:date]; } +// Can be called from any thread. +// - (NSString *) getDeviceOrientation { - switch ([UIDevice currentDevice].orientation) { - case UIDeviceOrientationUnknown: - return @"Unknown"; - break; - case UIDeviceOrientationPortrait: - return @"Portrait"; - break; - case UIDeviceOrientationPortraitUpsideDown: - return @"PortraitUpsideDown"; - break; - case UIDeviceOrientationLandscapeLeft: - return @"LandscapeLeft"; - break; - case UIDeviceOrientationLandscapeRight: - return @"LandscapeRight"; - break; - case UIDeviceOrientationFaceUp: - return @"FaceUp"; - break; - case UIDeviceOrientationFaceDown: - return @"FaceDown"; - break; - default: - return @"Default - Unknown"; - break; + __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; } -- (int) getContentSizeScale { - NSString *sc = [UIApplication sharedApplication].preferredContentSizeCategory; - - if ([sc isEqualToString:UIContentSizeCategoryExtraSmall]) { - return -3; - } else if ([sc isEqualToString:UIContentSizeCategorySmall]) { - return -2; - } else if ([sc isEqualToString:UIContentSizeCategoryMedium]) { - return -1; - } else if ([sc isEqualToString:UIContentSizeCategoryLarge]) { - return 0; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraLarge]) { - return 1; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { - return 2; - } else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { - return 3; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { - return -11; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { - return 10; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { - return 11; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { - return 12; - } else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { - return 13; +// Can be called from any thread. +// +- (NSInteger) getContentSizeScale { + __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 -9999; -} + return result; +} +// Can be called from any thread. +// - (NSString *)getSysInfoByName:(char *)typeSpecifier { size_t size; @@ -325,6 +486,8 @@ NSNumber *scale; return results; } +// Can be called from any thread. +// - (NSString *) getWifiNetworkName { NSString *ssid = @""; @@ -343,6 +506,8 @@ NSNumber *scale; return ssid; } +// Can be called from any thread. +// - (NSString *) getCurrentCellularNetworkConnectionType { CTTelephonyNetworkInfo *telephonyInfo = [CTTelephonyNetworkInfo new]; NSString *radioTech = telephonyInfo.currentRadioAccessTechnology; @@ -376,6 +541,52 @@ NSNumber *scale; } } +// Can be called from any thread. +// ++ (NSString *) checkEmailEnabled { + __block NSString *result; + + NSString *(^mailCheckBlock)(void) = ^{ + NSString *email = @"Unknown"; + Class MFMailComposeViewController = NSClassFromString(@"MFMailComposeViewController"); + if (MFMailComposeViewController) { + SEL canSendMail = NSSelectorFromString(@"canSendMail"); + BOOL sendMail = ((BOOL (*)(id, SEL))[MFMailComposeViewController methodForSelector:canSendMail]) + (MFMailComposeViewController, canSendMail); + email = [NSString stringWithFormat:@"%i", sendMail]; + } + return email; + }; + + if ( ! [[NSThread currentThread] isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + result = mailCheckBlock(); + }); + } else { + result = mailCheckBlock(); + } + + return result; +} + +// Can be called from any thread. +// ++ (BOOL) checkPushEnabled { + __block BOOL result; + + BOOL (^pushCheckBlock)(void) = ^{ + return [[UIApplication sharedApplication] isRegisteredForRemoteNotifications]; + }; + if ( ! [[NSThread currentThread] isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + result = pushCheckBlock(); + }); + } else { + result = pushCheckBlock(); + } -@end
\ No newline at end of file + return result; +} + +@end diff --git a/platform/ios/MGLMetricsLocationManager.m b/platform/ios/MGLMetricsLocationManager.m index 19210fddca..bdf6e14ae9 100644 --- a/platform/ios/MGLMetricsLocationManager.m +++ b/platform/ios/MGLMetricsLocationManager.m @@ -1,29 +1,17 @@ -// -// MBLocationManager.m -// Hermes -// -// 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 -// -// Created by Brad Leege on 3/8/15. -// Copyright (c) 2015 Mapbox. All rights reserved. -// - -#import "MGLMetricsLocationManager.h" -#import "CoreLocation/CoreLocation.h" #import "MGLMapboxEvents.h" +#import "MGLMetricsLocationManager.h" -@interface MGLMetricsLocationManager() +#import <CoreLocation/CoreLocation.h> -@property (atomic) CLLocationManager *locationManager; +@interface MGLMetricsLocationManager() <CLLocationManagerDelegate> + +@property (nonatomic) CLLocationManager *locationManager; @end @implementation MGLMetricsLocationManager -static MGLMetricsLocationManager *sharedManager = nil; - -- (id) init { +- (instancetype) init { if (self = [super init]) { _locationManager = [[CLLocationManager alloc] init]; _locationManager.distanceFilter = 2; @@ -34,43 +22,31 @@ static MGLMetricsLocationManager *sharedManager = nil; return self; } -+ (id)sharedManager { ++ (instancetype)sharedManager { static dispatch_once_t onceToken; + static MGLMetricsLocationManager *sharedManager; dispatch_once(&onceToken, ^{ sharedManager = [[self alloc] init]; }); return sharedManager; } -- (BOOL) isAuthorizedStatusDetermined { - return ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusNotDetermined); -} - -- (void) requestAlwaysAuthorization { - if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) { - [self.locationManager requestAlwaysAuthorization]; - } else { - // This is iOS 7 or below so Starting Location Updates will trigger authorization request - [self startUpdatingLocation]; - } -} - -- (void) startUpdatingLocation { - [self.locationManager startUpdatingLocation]; ++ (void) startUpdatingLocation { + [[MGLMetricsLocationManager sharedManager].locationManager startUpdatingLocation]; } -- (void) stopUpdatingLocation { - [self.locationManager stopUpdatingLocation]; ++ (void) stopUpdatingLocation { + [[MGLMetricsLocationManager sharedManager].locationManager stopUpdatingLocation]; } #pragma mark CLLocationManagerDelegate - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { // Iterate through locations to pass all data for (CLLocation *loc in locations) { - NSMutableDictionary *evt = [[NSMutableDictionary alloc] init]; - [evt setValue:[[NSNumber alloc] initWithDouble:loc.coordinate.latitude] forKey:@"lat"]; - [evt setValue:[[NSNumber alloc] initWithDouble:loc.coordinate.longitude] forKey:@"lng"]; - [[MGLMapboxEvents sharedManager] pushEvent:@"location" withAttributes:evt]; + [MGLMapboxEvents pushEvent:MGLEventMapLocation withAttributes:@{ + @"lat": @(loc.coordinate.latitude), + @"lng": @(loc.coordinate.longitude) + }]; } } @@ -85,18 +61,18 @@ static MGLMetricsLocationManager *sharedManager = nil; break; case kCLAuthorizationStatusDenied: newStatus = @"User Explcitly Denied"; - [[MGLMetricsLocationManager sharedManager] stopUpdatingLocation]; + [MGLMetricsLocationManager stopUpdatingLocation]; break; case kCLAuthorizationStatusAuthorized: newStatus = @"User Has Authorized / Authorized Always"; - [[MGLMetricsLocationManager sharedManager] startUpdatingLocation]; + [MGLMetricsLocationManager startUpdatingLocation]; break; // case kCLAuthorizationStatusAuthorizedAlways: // newStatus = @"Not Determined"; // break; case kCLAuthorizationStatusAuthorizedWhenInUse: newStatus = @"User Has Authorized When In Use Only"; - [[MGLMetricsLocationManager sharedManager] startUpdatingLocation]; + [MGLMetricsLocationManager startUpdatingLocation]; break; default: newStatus = @"Unknown"; |