// // 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 @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; NSString *anonId = nil; NSDateFormatter *rfc3339DateFormatter = nil; NSString *model; NSString *iOSVersion; NSString *carrier; NSNumber *scale; - (id) init { self = [super init]; if (self) { _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]; } // Get Vendor ID 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 { // TODO Add Opt Out Checking When Built /** if (![[NSUserDefaults standardUserDefaults] boolForKey:@"mapbox_metrics_enabled_preference"]) { NSLog(@"Mapbox Metrics are not enabled, so return without sending in data."); return; } */ // TODO - Add Metrics Disabled App Wide Check 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"]; 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; } if (_flushAt > [_queue count]) { NSLog(@"flush() flushAt (%lu) is greater than current queue count (%lu) so just return", _flushAt, [_queue count]); return; } // Create Array of Events to push to the Server NSRange theRange = NSMakeRange(0, (int)_flushAt); 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; } } - (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; } @end