diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index a02a96eb1..73f611a8e 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -17,9 +17,13 @@ '../platform/darwin/image.mm', '../platform/darwin/reachability.m', '../include/mbgl/ios/MapboxGL.h', + '../include/mbgl/ios/MGLMapboxEvents.h', + '../platform/ios/MGLMapboxEvents.m', '../include/mbgl/ios/MGLMapView.h', '../platform/ios/MGLMapView.mm', '../include/mbgl/ios/MGLAnnotation.h', + '../include/mbgl/ios/MGLMetricsLocationManager.h', + '../platform/ios/MGLMetricsLocationManager.m', '../include/mbgl/ios/MGLStyleFunctionValue.h', '../platform/ios/MGLStyleFunctionValue.m', '../include/mbgl/ios/MGLTypes.h', @@ -43,6 +47,7 @@ 'ldflags': [ '-framework CoreGraphics', '-framework CoreLocation', + '-framework CoreTelephony', '-framework ImageIO', '-framework GLKit', '-framework MobileCoreServices', diff --git a/include/mbgl/ios/MGLMapboxEvents.h b/include/mbgl/ios/MGLMapboxEvents.h new file mode 100644 index 000000000..bb28ec26c --- /dev/null +++ b/include/mbgl/ios/MGLMapboxEvents.h @@ -0,0 +1,26 @@ +// +// MapboxEvents.h +// MapboxEvents +// +// Created by Brad Leege on 3/5/15. +// Copyright (c) 2015 Mapbox. All rights reserved. +// + +#import + +@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; + ++ (id)sharedManager; + +- (void) pushEvent:(NSString *)event withAttributes:(NSDictionary *)attributeDictionary; + +- (void) flush; + +@end \ No newline at end of file diff --git a/include/mbgl/ios/MGLMetricsLocationManager.h b/include/mbgl/ios/MGLMetricsLocationManager.h new file mode 100644 index 000000000..ce04ae9ef --- /dev/null +++ b/include/mbgl/ios/MGLMetricsLocationManager.h @@ -0,0 +1,22 @@ +// +// MBLocationManager.h +// Hermes +// +// Created by Brad Leege on 3/8/15. +// Copyright (c) 2015 Mapbox. All rights reserved. +// + +#import +#import "CoreLocation/CoreLocation.h" + +@interface MGLMetricsLocationManager : NSObject + ++ (id)sharedManager; + +- (BOOL) isAuthorizedStatusDetermined; +- (void) requestAlwaysAuthorization; + +- (void) startUpdatingLocation; +- (void) stopUpdatingLocation; + +@end diff --git a/ios/app/Settings.bundle/Root.plist b/ios/app/Settings.bundle/Root.plist new file mode 100644 index 000000000..4e50f0ef3 --- /dev/null +++ b/ios/app/Settings.bundle/Root.plist @@ -0,0 +1,47 @@ + + + + + PreferenceSpecifiers + + + Title + Privacy Settings + Type + PSGroupSpecifier + + + + DefaultValue + + Key + mapbox_metrics_enabled_preference + Title + Mapbox Metrics + Type + PSToggleSwitchSpecifier + TrueValue + + FalseValue + + + + StringsTable + Root + + diff --git a/ios/app/Settings.bundle/en.lproj/Root.strings b/ios/app/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 000000000..8cd87b9d6 Binary files /dev/null and b/ios/app/Settings.bundle/en.lproj/Root.strings differ diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index bb08f9b77..ce5997e0a 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -23,6 +23,8 @@ #import "NSDictionary+MGLAdditions.h" #import +#import "MGLMapboxEvents.h" +#import "MGLMetricsLocationManager.h" // Returns the path to the default cache database on this system. const std::string &defaultCacheDatabase() { @@ -179,6 +181,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; if (accessToken) { mbglMap->setAccessToken((std::string)[accessToken cStringUsingEncoding:[NSString defaultCStringEncoding]]); + [[MGLMapboxEvents sharedManager] setToken:accessToken]; } } @@ -217,6 +220,17 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; // setup accessibility // self.accessibilityLabel = @"Map"; + + // setup Metrics + MGLMapboxEvents *events = [MGLMapboxEvents sharedManager]; + NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; + NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if (appName != nil) { + events.appName = appName; + } + if (appVersion != nil) { + events.appVersion = appVersion; + } // create GL view // @@ -357,6 +371,9 @@ 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 MBLocationManager for metrics + [MGLMetricsLocationManager sharedManager]; + // set initial position // mbglMap->setLatLngZoom(mbgl::LatLng(0, 0), mbglMap->getMinZoom()); @@ -369,6 +386,28 @@ 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_DEFAULT, 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"]; + [[MGLMapboxEvents sharedManager] pushEvent:@"map.load" withAttributes:evt]; + + [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"]; + }); + return YES; } @@ -546,6 +585,9 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)appDidBackground:(NSNotification *)notification { + // Flush Any Events Still In Queue + [[MGLMapboxEvents sharedManager] flush]; + mbglMap->stop(); [self.glView deleteDrawable]; @@ -580,6 +622,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handlePanGesture:(UIPanGestureRecognizer *)pan { + [self trackGestureEvent:@"Pan" forRecognizer:pan]; + if ( ! self.isScrollEnabled) return; mbglMap->cancelTransitions(); @@ -631,11 +675,23 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [weakSelf notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)]; }]; } + + // 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"]; + + [[MGLMapboxEvents sharedManager] pushEvent:@"map.dragend" withAttributes:dict]; } } - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch { + [self trackGestureEvent:@"Pinch" forRecognizer:pinch]; + if ( ! self.isZoomEnabled) return; if (mbglMap->getZoom() <= mbglMap->getMinZoom() && pinch.scale < 1) return; @@ -670,6 +726,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate { + [self trackGestureEvent:@"Rotation" forRecognizer:rotate]; + if ( ! self.isRotateEnabled) return; mbglMap->cancelTransitions(); @@ -708,8 +766,10 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap { + [self trackGestureEvent:@"SingleTap" forRecognizer:singleTap]; + CGPoint tapPoint = [singleTap locationInView:self]; - + // tolerances based on touch size & typical marker aspect ratio CGFloat toleranceWidth = 50; CGFloat toleranceHeight = 75; @@ -820,6 +880,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleDoubleTapGesture:(UITapGestureRecognizer *)doubleTap { + [self trackGestureEvent:@"DoubleTap" forRecognizer:doubleTap]; + if ( ! self.isZoomEnabled) return; mbglMap->cancelTransitions(); @@ -845,6 +907,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleTwoFingerTapGesture:(UITapGestureRecognizer *)twoFingerTap { + [self trackGestureEvent:@"TwoFingerTap" forRecognizer:twoFingerTap]; + if ( ! self.isZoomEnabled) return; if (mbglMap->getZoom() == mbglMap->getMinZoom()) return; @@ -872,6 +936,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleQuickZoomGesture:(UILongPressGestureRecognizer *)quickZoom { + [self trackGestureEvent:@"QuickZoom" forRecognizer:quickZoom]; + if ( ! self.isZoomEnabled) return; mbglMap->cancelTransitions(); @@ -907,6 +973,20 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]); } +- (void) trackGestureEvent:(NSString *) gesture forRecognizer:(UIGestureRecognizer *) recognizer +{ + // 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]; +} + #pragma mark - Properties - #pragma clang diagnostic push diff --git a/platform/ios/MGLMapboxEvents.m b/platform/ios/MGLMapboxEvents.m new file mode 100644 index 000000000..a231b5115 --- /dev/null +++ b/platform/ios/MGLMapboxEvents.m @@ -0,0 +1,337 @@ +// +// 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 +#import +#import +#include +#import + +@interface MGLMapboxEvents() + +@property (atomic) NSMutableArray *queue; +@property (atomic) NSString *instance; +@property (atomic) NSString *anonid; +@property (atomic) NSTimer *timer; +@property (atomic) NSString *userAgent; + +@end + +@implementation MGLMapboxEvents + +static MGLMapboxEvents *sharedManager = nil; + +NSDateFormatter *rfc3339DateFormatter = nil; +NSString *model; +NSString *iOSVersion; +NSString *carrier; +NSNumber *scale; + +- (id) init { + self = [super init]; + if (self) { + + // Put Settings bundle into memory + NSString *settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"]; + if(!settingsBundle) { + NSLog(@"Could not find Settings.bundle"); + } else { + NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]]; + NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"]; + NSMutableDictionary *defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]]; + for(NSDictionary *prefSpecification in preferences) { + NSString *key = [prefSpecification objectForKey:@"Key"]; + if(key && [[prefSpecification allKeys] containsObject:@"DefaultValue"]) { + [defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key]; + } + } + + [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister]; + } + + // Configure Events Infrastructure + _queue = [[NSMutableArray alloc] init]; + _flushAt = 20; + _flushAfter = 10000; + _api = @"https://api.tiles.mapbox.com"; + _token = nil; + _instance = [[NSUUID UUID] UUIDString]; + Class ASIdentifierManagerClass = NSClassFromString(@"ASIdentifierManager"); + if (ASIdentifierManagerClass) { + SEL sharedManagerSelector = NSSelectorFromString(@"sharedManager"); + id sharedManager = ((id (*)(id, SEL))[ASIdentifierManagerClass methodForSelector:sharedManagerSelector])(ASIdentifierManagerClass, sharedManagerSelector); + // Add check here + SEL isAdvertisingTrackingEnabledSelector = NSSelectorFromString(@"isAdvertisingTrackingEnabled"); + BOOL trackingEnabled = ((BOOL (*)(id, SEL))[sharedManager methodForSelector:isAdvertisingTrackingEnabledSelector])(sharedManager, isAdvertisingTrackingEnabledSelector); + if (trackingEnabled) { + SEL advertisingIdentifierSelector = NSSelectorFromString(@"advertisingIdentifier"); + NSUUID *uuid = ((NSUUID* (*)(id, SEL))[sharedManager methodForSelector:advertisingIdentifierSelector])(sharedManager, advertisingIdentifierSelector); + _anonid = [uuid UUIDString]; + } else { + _anonid = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + } + } else { + _anonid = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + } + + 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]; + } else { + scale = [[NSNumber alloc] initWithFloat:[UIScreen mainScreen].scale]; + } + CTCarrier *carrierVendor = [[[CTTelephonyNetworkInfo alloc] init] subscriberCellularProvider]; + carrier = [carrierVendor carrierName]; + + _userAgent = @"MapboxEventsiOS/1.0"; + + // 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':'ss'Z'"]; + [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + } + return self; +} + ++ (id)sharedManager { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedManager = [[self alloc] init]; + }); + return sharedManager; +} + +- (void) pushEvent:(NSString *)event withAttributes:(NSDictionary *)attributeDictionary { + + // Opt Out Checking When Built + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"mapbox_metrics_enabled_preference"]) { + NSLog(@"Mapbox Metrics are not enabled, so clear any currently stored events, and return without sending in data."); + [_queue removeAllObjects]; + return; + } + + // Add Metrics Disabled App Wide Check + if ([[NSUserDefaults standardUserDefaults] objectForKey:@"mapbox_metrics_disabled"] != nil) { + NSLog(@"Mapbox Metrics have been disabled for this app."); + [_queue 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"]; + + // mapbox-events-ios stock attributes + [evt setValue:[rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"deviceTimestamp"]; + [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 getWifiNetworkName] forKey:@"wifi"]; + [evt setValue:[NSNumber numberWithInt:[self getContentSizeScale]] forKey:@"accessibilityFontScale"]; + + for (NSString *key in [attributeDictionary allKeys]) { + [evt setObject:[attributeDictionary valueForKey:key] forKey:key]; + } + + // Make Immutable Version + NSDictionary *finalEvent = [NSDictionary dictionaryWithDictionary:evt]; + + // Put On The Queue + [self.queue addObject:finalEvent]; + + // Has Flush Limit Been Reached? + if ((int)_queue.count >= (int)_flushAt) { + [self flush]; + } + + // Reset Timer (Initial Starting of Timer after first event is pushed) + [self startTimer]; +} + +- (void) flush { + if (_token == nil) { + NSLog(@"token hasn't been set yet, so no events can be sent. return"); + return; + } + + int upper = (int)_flushAt; + if (_flushAt > [_queue count]) { + if ([_queue count] == 0) { + return; + } + upper = (int)[_queue count]; + } + + // Create Array of Events to push to the Server + NSRange theRange = NSMakeRange(0, upper); + NSArray *events = [_queue subarrayWithRange:theRange]; + + // Update Queue to remove events sent to server + [_queue removeObjectsInRange:theRange]; + + // Send Array of Events to Server + [self postEvents:events]; + +} + +- (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]; + + // Send non blocking HTTP Request to server + [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:nil]; + } +} + +- (void) startTimer { + // Stop Timer if it already exists + if (_timer) { + [_timer invalidate]; + _timer = nil; + } + + // Start New Timer + NSTimeInterval interval = (double)((NSInteger)_flushAfter); + _timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(flush) userInfo:nil repeats:YES]; +} + +- (NSString *) getUserAgent { + + if (_appName != nil && _appVersion != nil && ([_userAgent rangeOfString:_appName].location == NSNotFound)) { + _userAgent = [NSString stringWithFormat:@"%@/%@ %@", _appName, _appVersion, _userAgent]; + } + return _userAgent; +} + +- (NSString *) formatDate:(NSDate *)date { + return [rfc3339DateFormatter stringFromDate:date]; +} + +- (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; + } +} + +- (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; + } + return -9999; +} + + +- (NSString *)getSysInfoByName:(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; +} + +- (NSString *) getWifiNetworkName { + + NSString *ssid = @""; + CFArrayRef interfaces = CNCopySupportedInterfaces(); + if (interfaces) { + NSDictionary *info = (__bridge NSDictionary *)CNCopyCurrentNetworkInfo(CFArrayGetValueAtIndex(interfaces, 0)); + if (info) { + ssid = info[@"SSID"]; + } else { + ssid = @"<>"; + } + } else { + ssid = @"<>"; + } + + return ssid; +} + + +@end \ No newline at end of file diff --git a/platform/ios/MGLMetricsLocationManager.m b/platform/ios/MGLMetricsLocationManager.m new file mode 100644 index 000000000..5eac0370d --- /dev/null +++ b/platform/ios/MGLMetricsLocationManager.m @@ -0,0 +1,129 @@ +// +// 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" + +@interface MGLMetricsLocationManager() + +@property (atomic) CLLocationManager *locationManager; + +@end + +@implementation MGLMetricsLocationManager + +static MGLMetricsLocationManager *sharedManager = nil; + +- (id) init { + if (self = [super init]) { + + NSString *settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"]; + if(!settingsBundle) { + NSLog(@"Could not find Settings.bundle"); + } else { + NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]]; + NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"]; + NSMutableDictionary *defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]]; + for(NSDictionary *prefSpecification in preferences) { + NSString *key = [prefSpecification objectForKey:@"Key"]; + if(key && [[prefSpecification allKeys] containsObject:@"DefaultValue"]) { + [defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key]; + } + } + + [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister]; + } + + _locationManager = [[CLLocationManager alloc] init]; + _locationManager.distanceFilter = 2; + _locationManager.desiredAccuracy = kCLLocationAccuracyBest; + _locationManager.pausesLocationUpdatesAutomatically = YES; + [_locationManager setDelegate:self]; + } + return self; +} + ++ (id)sharedManager { + static dispatch_once_t onceToken; + 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) stopUpdatingLocation { + [self.locationManager stopUpdatingLocation]; +} + +#pragma mark CLLocationManagerDelegate +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + CLLocation *loc = (CLLocation *)[locations objectAtIndex:0]; + NSLog(@"didUpdateLocations() called with %lu location in array. First Location = %f, %f", (unsigned long)locations.count, loc.coordinate.latitude, loc.coordinate.longitude); + + // 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]; + } +} + +- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { + NSString *newStatus = nil; + switch (status) { + case kCLAuthorizationStatusNotDetermined: + newStatus = @"User Hasn't Determined Yet"; + break; + case kCLAuthorizationStatusRestricted: + newStatus = @"Restricted and Can't Be Changed By User"; + break; + case kCLAuthorizationStatusDenied: + newStatus = @"User Explcitly Denied"; + [[MGLMetricsLocationManager sharedManager] stopUpdatingLocation]; + break; + case kCLAuthorizationStatusAuthorized: + newStatus = @"User Has Authorized / Authorized Always"; + [[MGLMetricsLocationManager sharedManager] startUpdatingLocation]; + break; + // case kCLAuthorizationStatusAuthorizedAlways: + // newStatus = @"Not Determined"; + // break; + case kCLAuthorizationStatusAuthorizedWhenInUse: + newStatus = @"User Has Authorized When In Use Only"; + [[MGLMetricsLocationManager sharedManager] startUpdatingLocation]; + break; + default: + newStatus = @"Unknown"; + break; + } + NSLog(@"MGLMetricsLocationManager didChangeAuthorizationStatus() called. New Status = %@", newStatus); +} + +@end