792 lines
33 KiB
Objective-C
792 lines
33 KiB
Objective-C
#import "MGLMapboxEvents.h"
|
|
#import <UIKit/UIKit.h>
|
|
#import <CoreLocation/CoreLocation.h>
|
|
#import "MGLAccountManager.h"
|
|
#import "NSProcessInfo+MGLAdditions.h"
|
|
#import "NSException+MGLAdditions.h"
|
|
#import "MGLAPIClient.h"
|
|
#import "MGLLocationManager.h"
|
|
|
|
#include <mbgl/platform/darwin/reachability.h>
|
|
#include <sys/sysctl.h>
|
|
|
|
// Event types
|
|
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 MGLEventTypeLocalDebug = @"debug";
|
|
|
|
// Gestures
|
|
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";
|
|
|
|
// Event keys
|
|
NSString *const MGLEventKeyLatitude = @"lat";
|
|
NSString *const MGLEventKeyLongitude = @"lng";
|
|
NSString *const MGLEventKeyZoomLevel = @"zoom";
|
|
NSString *const MGLEventKeySpeed = @"speed";
|
|
NSString *const MGLEventKeyCourse = @"course";
|
|
NSString *const MGLEventKeyGestureID = @"gesture";
|
|
NSString *const MGLEventKeyLocalDebugDescription = @"debug.description";
|
|
|
|
static NSString *const MGLEventKeyEvent = @"event";
|
|
static NSString *const MGLEventKeyCreated = @"created";
|
|
static NSString *const MGLEventKeyVendorID = @"userId";
|
|
static NSString *const MGLEventKeyModel = @"model";
|
|
static NSString *const MGLEventKeyEnabledTelemetry = @"enabled.telemetry";
|
|
static NSString *const MGLEventKeyOperatingSystem = @"operatingSystem";
|
|
static NSString *const MGLEventKeyResolution = @"resolution";
|
|
static NSString *const MGLEventKeyAccessibilityFontScale = @"accessibilityFontScale";
|
|
static NSString *const MGLEventKeyOrientation = @"orientation";
|
|
static NSString *const MGLEventKeyBatteryLevel = @"batteryLevel";
|
|
static NSString *const MGLEventKeyPluggedIn = @"pluggedIn";
|
|
static NSString *const MGLEventKeyWifi = @"wifi";
|
|
static NSString *const MGLEventKeySource = @"source";
|
|
static NSString *const MGLEventKeySessionId = @"sessionId";
|
|
static NSString *const MGLEventKeyApplicationState = @"applicationState";
|
|
static NSString *const MGLEventKeyAltitude = @"altitude";
|
|
|
|
// SDK event source
|
|
static NSString *const MGLEventSource = @"mapbox";
|
|
|
|
// Event application state
|
|
static NSString *const MGLApplicationStateForeground = @"Foreground";
|
|
static NSString *const MGLApplicationStateBackground = @"Background";
|
|
static NSString *const MGLApplicationStateInactive = @"Inactive";
|
|
static NSString *const MGLApplicationStateUnknown = @"Unknown";
|
|
|
|
const NSUInteger MGLMaximumEventsPerFlush = 180;
|
|
const NSTimeInterval MGLFlushInterval = 180;
|
|
|
|
@interface MGLMapboxEventsData : NSObject
|
|
|
|
@property (nonatomic) NSString *vendorId;
|
|
@property (nonatomic) NSString *model;
|
|
@property (nonatomic) NSString *iOSVersion;
|
|
@property (nonatomic) 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;
|
|
}
|
|
|
|
- (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
|
|
|
|
@interface MGLMapboxEvents () <MGLLocationManagerDelegate>
|
|
|
|
@property (nonatomic) MGLMapboxEventsData *data;
|
|
@property (nonatomic, copy) NSString *appBundleId;
|
|
@property (nonatomic, readonly) NSString *instanceID;
|
|
@property (nonatomic, copy) NSString *dateForDebugLogFile;
|
|
@property (nonatomic) NSDateFormatter *rfc3339DateFormatter;
|
|
@property (nonatomic) MGLAPIClient *apiClient;
|
|
@property (nonatomic) BOOL usesTestServer;
|
|
@property (nonatomic) BOOL canEnableDebugLogging;
|
|
@property (nonatomic, getter=isPaused) BOOL paused;
|
|
@property (nonatomic) NS_MUTABLE_ARRAY_OF(MGLMapboxEventAttributes *) *eventQueue;
|
|
@property (nonatomic) dispatch_queue_t serialQueue;
|
|
@property (nonatomic) dispatch_queue_t debugLogSerialQueue;
|
|
@property (nonatomic) MGLLocationManager *locationManager;
|
|
@property (nonatomic) NSTimer *timer;
|
|
@property (nonatomic) NSDate *instanceIDRotationDate;
|
|
@property (nonatomic) NSDate *nextTurnstileSendDate;
|
|
|
|
@end
|
|
|
|
@implementation MGLMapboxEvents {
|
|
NSString *_instanceID;
|
|
}
|
|
|
|
+ (void)initialize {
|
|
if (self == [MGLMapboxEvents class]) {
|
|
NSBundle *bundle = [NSBundle mainBundle];
|
|
NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:@"MGLMapboxAccountType"];
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
|
|
@"MGLMapboxAccountType": accountTypeNumber ?: @0,
|
|
@"MGLMapboxMetricsEnabled": @YES,
|
|
@"MGLMapboxMetricsDebugLoggingEnabled": @NO,
|
|
}];
|
|
}
|
|
}
|
|
|
|
+ (BOOL)isEnabled {
|
|
#if TARGET_OS_SIMULATOR
|
|
return NO;
|
|
#else
|
|
BOOL isLowPowerModeEnabled = NO;
|
|
if ([NSProcessInfo instancesRespondToSelector:@selector(isLowPowerModeEnabled)]) {
|
|
isLowPowerModeEnabled = [[NSProcessInfo processInfo] isLowPowerModeEnabled];
|
|
}
|
|
return ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsEnabled"] &&
|
|
[[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0 &&
|
|
!isLowPowerModeEnabled);
|
|
#endif
|
|
}
|
|
|
|
|
|
- (BOOL)debugLoggingEnabled {
|
|
return (self.canEnableDebugLogging &&
|
|
[[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]);
|
|
}
|
|
|
|
- (instancetype) init {
|
|
self = [super init];
|
|
if (self) {
|
|
_appBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
|
_apiClient = [[MGLAPIClient alloc] init];
|
|
|
|
NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
|
|
_serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
|
|
|
|
_locationManager = [[MGLLocationManager alloc] init];
|
|
_locationManager.delegate = self;
|
|
_paused = YES;
|
|
[self resumeMetricsCollection];
|
|
|
|
// 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;
|
|
|
|
// Configure logging
|
|
if ([self isProbablyAppStoreBuild]) {
|
|
self.canEnableDebugLogging = NO;
|
|
|
|
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]) {
|
|
NSLog(@"Telemetry logging is only enabled in non-app store builds.");
|
|
}
|
|
} else {
|
|
self.canEnableDebugLogging = YES;
|
|
}
|
|
|
|
// Watch for changes to telemetry settings by the user
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationWillEnterForegroundNotification object:nil];
|
|
|
|
// Watch for Low Power Mode change events
|
|
if (&NSProcessInfoPowerStateDidChangeNotification != NULL) {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:NSProcessInfoPowerStateDidChangeNotification object:nil];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
// 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;
|
|
dispatch_once(&onceToken, ^{
|
|
_sharedManager = [[self alloc] init];
|
|
});
|
|
return _sharedManager;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
[self pauseMetricsCollection];
|
|
}
|
|
|
|
- (NSString *)instanceID {
|
|
if (self.instanceIDRotationDate && [[NSDate date] timeIntervalSinceDate:self.instanceIDRotationDate] >= 0) {
|
|
_instanceID = nil;
|
|
}
|
|
if (!_instanceID) {
|
|
_instanceID = [[NSUUID UUID] UUIDString];
|
|
NSTimeInterval twentyFourHourTimeInterval = 24 * 3600;
|
|
self.instanceIDRotationDate = [[NSDate date] dateByAddingTimeInterval:twentyFourHourTimeInterval];
|
|
}
|
|
return _instanceID;
|
|
}
|
|
|
|
- (void)userDefaultsDidChange:(NSNotification *)notification {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self pauseOrResumeMetricsCollectionIfRequired];
|
|
});
|
|
}
|
|
|
|
- (void)pauseOrResumeMetricsCollectionIfRequired {
|
|
// Prevent blue status bar when host app has `when in use` permission only and it is not in foreground
|
|
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse &&
|
|
[UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
|
|
[self pauseMetricsCollection];
|
|
return;
|
|
}
|
|
|
|
// Toggle pause based on current pause state, user opt-out state, and low-power state.
|
|
BOOL enabled = [[self class] isEnabled];
|
|
if (self.paused && enabled) {
|
|
[self resumeMetricsCollection];
|
|
} else if (!self.paused && !enabled) {
|
|
[self pauseMetricsCollection];
|
|
}
|
|
}
|
|
|
|
- (void)pauseMetricsCollection {
|
|
if (self.paused) {
|
|
return;
|
|
}
|
|
|
|
self.paused = YES;
|
|
[self.timer invalidate];
|
|
self.timer = nil;
|
|
[self.eventQueue removeAllObjects];
|
|
self.data = nil;
|
|
|
|
[self.locationManager stopUpdatingLocation];
|
|
}
|
|
|
|
- (void)resumeMetricsCollection {
|
|
if (!self.paused || ![[self class] isEnabled]) {
|
|
return;
|
|
}
|
|
|
|
self.paused = NO;
|
|
self.data = [[MGLMapboxEventsData alloc] init];
|
|
|
|
[self.locationManager startUpdatingLocation];
|
|
}
|
|
|
|
+ (void)flush {
|
|
[[MGLMapboxEvents sharedManager] flush];
|
|
}
|
|
|
|
- (void)flush {
|
|
if ([MGLAccountManager accessToken] == nil) {
|
|
return;
|
|
}
|
|
|
|
if ([self.eventQueue count] == 0) {
|
|
return;
|
|
}
|
|
|
|
NSArray *events = [NSArray arrayWithArray:self.eventQueue];
|
|
[self.eventQueue removeAllObjects];
|
|
|
|
[self postEvents:events];
|
|
|
|
if (self.timer) {
|
|
[self.timer invalidate];
|
|
self.timer = nil;
|
|
}
|
|
|
|
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"flush"}];
|
|
}
|
|
|
|
- (void)pushTurnstileEvent {
|
|
if (self.nextTurnstileSendDate && [[NSDate date] timeIntervalSinceDate:self.nextTurnstileSendDate] < 0) {
|
|
return;
|
|
}
|
|
|
|
NSString *vendorID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
|
|
if (!vendorID) {
|
|
return;
|
|
}
|
|
|
|
NSDictionary *turnstileEventAttributes = @{MGLEventKeyEvent: MGLEventTypeAppUserTurnstile,
|
|
MGLEventKeyCreated: [self.rfc3339DateFormatter stringFromDate:[NSDate date]],
|
|
MGLEventKeyVendorID: vendorID,
|
|
MGLEventKeyEnabledTelemetry: @([[self class] isEnabled])};
|
|
|
|
if ([MGLAccountManager accessToken] == nil) {
|
|
return;
|
|
}
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
[self.apiClient postEvent:turnstileEventAttributes completionHandler:^(NSError * _Nullable error) {
|
|
__strong __typeof__(weakSelf) strongSelf = weakSelf;
|
|
if (error) {
|
|
[strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error",
|
|
@"error": error}];
|
|
return;
|
|
}
|
|
[strongSelf writeEventToLocalDebugLog:turnstileEventAttributes];
|
|
[strongSelf updateNextTurnstileSendDate];
|
|
}];
|
|
}
|
|
|
|
- (void)updateNextTurnstileSendDate {
|
|
// Find the time a day from now (sometime tomorrow)
|
|
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
|
|
NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
|
|
dayComponent.day = 1;
|
|
NSDate *sometimeTomorrow = [calendar dateByAddingComponents:dayComponent toDate:[NSDate date] options:0];
|
|
|
|
// Find the start of tomorrow and use that as the next turnstile send date. The effect of this is that
|
|
// turnstile events can be sent as much as once per calendar day and always at the start of a session
|
|
// when a map load happens.
|
|
NSDate *startOfTomorrow = nil;
|
|
[calendar rangeOfUnit:NSCalendarUnitDay startDate:&startOfTomorrow interval:nil forDate:sometimeTomorrow];
|
|
self.nextTurnstileSendDate = startOfTomorrow;
|
|
}
|
|
|
|
+ (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
[[MGLMapboxEvents sharedManager] pushEvent:event withAttributes:attributeDictionary];
|
|
}
|
|
|
|
- (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
if (!event) {
|
|
return;
|
|
}
|
|
|
|
if ([event isEqualToString:MGLEventTypeMapLoad]) {
|
|
[self pushTurnstileEvent];
|
|
}
|
|
|
|
if (self.paused) {
|
|
return;
|
|
}
|
|
|
|
MGLMapboxEventAttributes *fullyFormedEvent = [self fullyFormedEventForEvent:event withAttributes:attributeDictionary];
|
|
if (fullyFormedEvent) {
|
|
[self.eventQueue addObject:fullyFormedEvent];
|
|
[self writeEventToLocalDebugLog:fullyFormedEvent];
|
|
// Has Flush Limit Been Reached?
|
|
if (self.eventQueue.count >= MGLMaximumEventsPerFlush) {
|
|
[self flush];
|
|
} else if (self.eventQueue.count == 1) {
|
|
// If this is first new event on queue start timer,
|
|
[self startTimer];
|
|
}
|
|
} else {
|
|
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Unknown event",
|
|
@"eventName": event,
|
|
@"event.attributes": attributeDictionary}];
|
|
}
|
|
}
|
|
|
|
#pragma mark Events
|
|
|
|
- (MGLMapboxEventAttributes *)fullyFormedEventForEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
if ([event isEqualToString:MGLEventTypeMapLoad]) {
|
|
return [self mapLoadEventWithAttributes:attributeDictionary];
|
|
} else if ([event isEqualToString:MGLEventTypeMapTap]) {
|
|
return [self mapClickEventWithAttributes:attributeDictionary];
|
|
} else if ([event isEqualToString:MGLEventTypeMapDragEnd]) {
|
|
return [self mapDragEndEventWithAttributes:attributeDictionary];
|
|
} else if ([event isEqualToString:MGLEventTypeLocation]) {
|
|
return [self locationEventWithAttributes:attributeDictionary];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (MGLMapboxEventAttributes *)locationEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
MGLMutableMapboxEventAttributes *attributes = [@{MGLEventKeyEvent: MGLEventTypeLocation,
|
|
MGLEventKeySource: MGLEventSource,
|
|
MGLEventKeySessionId: self.instanceID,
|
|
MGLEventKeyOperatingSystem: self.data.iOSVersion} mutableCopy];
|
|
[self addApplicationStateToAttributes:attributes];
|
|
return [self eventForAttributes:attributes attributeDictionary:attributeDictionary];
|
|
}
|
|
|
|
- (MGLMapboxEventAttributes *)mapLoadEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
MGLMutableMapboxEventAttributes *attributes = [@{MGLEventKeyEvent: MGLEventTypeMapLoad,
|
|
MGLEventKeyCreated: [self.rfc3339DateFormatter stringFromDate:[NSDate date]],
|
|
MGLEventKeyVendorID: self.data.vendorId,
|
|
MGLEventKeyModel: self.data.model,
|
|
MGLEventKeyOperatingSystem: self.data.iOSVersion,
|
|
MGLEventKeyResolution: @(self.data.scale),
|
|
MGLEventKeyAccessibilityFontScale: @([self contentSizeScale]),
|
|
MGLEventKeyOrientation: [self deviceOrientation],
|
|
MGLEventKeyBatteryLevel: @([self batteryLevel]),
|
|
MGLEventKeyWifi: @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi])} mutableCopy];
|
|
[self addBatteryStateToAttributes:attributes];
|
|
return [self eventForAttributes:attributes attributeDictionary:attributeDictionary];
|
|
}
|
|
|
|
- (MGLMapboxEventAttributes *)mapClickEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
MGLMutableMapboxEventAttributes *attributes = [self interactionEvent];
|
|
attributes[MGLEventKeyEvent] = MGLEventTypeMapTap;
|
|
return [self eventForAttributes:attributes attributeDictionary:attributeDictionary];
|
|
}
|
|
|
|
- (MGLMapboxEventAttributes *)mapDragEndEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
MGLMutableMapboxEventAttributes *attributes = [self interactionEvent];
|
|
attributes[MGLEventKeyEvent] = MGLEventTypeMapDragEnd;
|
|
return [self eventForAttributes:attributes attributeDictionary:attributeDictionary];
|
|
}
|
|
|
|
- (MGLMutableMapboxEventAttributes *)interactionEvent {
|
|
MGLMutableMapboxEventAttributes *attributes = [@{MGLEventKeyCreated: [self.rfc3339DateFormatter stringFromDate:[NSDate date]],
|
|
MGLEventKeyOrientation: [self deviceOrientation],
|
|
MGLEventKeyBatteryLevel: @([self batteryLevel]),
|
|
MGLEventKeyWifi: @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi])} mutableCopy];
|
|
[self addBatteryStateToAttributes:attributes];
|
|
return attributes;
|
|
}
|
|
|
|
- (MGLMapboxEventAttributes *)eventForAttributes:(MGLMutableMapboxEventAttributes *)attributes attributeDictionary:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
[attributes addEntriesFromDictionary:attributeDictionary];
|
|
return [attributes copy];
|
|
}
|
|
|
|
// Called implicitly from public use of +flush.
|
|
//
|
|
- (void)postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events {
|
|
if (self.paused) {
|
|
return;
|
|
}
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
dispatch_async(self.serialQueue, ^{
|
|
__strong __typeof__(weakSelf) strongSelf = weakSelf;
|
|
[self.apiClient postEvents:events completionHandler:^(NSError * _Nullable error) {
|
|
if (error) {
|
|
[strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error",
|
|
@"error": error}];
|
|
return;
|
|
}
|
|
[strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"post",
|
|
@"debug.eventsCount": @(events.count)}];
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (void)startTimer {
|
|
[self.timer invalidate];
|
|
self.timer = [NSTimer scheduledTimerWithTimeInterval:MGLFlushInterval
|
|
target:self
|
|
selector:@selector(flush)
|
|
userInfo:nil
|
|
repeats:YES];
|
|
}
|
|
|
|
- (NSInteger)batteryLevel {
|
|
return [[NSNumber numberWithFloat:roundf(100 * [UIDevice currentDevice].batteryLevel)] integerValue];
|
|
}
|
|
|
|
- (NSString *)deviceOrientation {
|
|
NSString *result;
|
|
|
|
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;
|
|
}
|
|
|
|
- (NSString *)applicationState {
|
|
switch ([UIApplication sharedApplication].applicationState) {
|
|
case UIApplicationStateActive:
|
|
return MGLApplicationStateForeground;
|
|
case UIApplicationStateInactive:
|
|
return MGLApplicationStateInactive;
|
|
case UIApplicationStateBackground:
|
|
return MGLApplicationStateBackground;
|
|
default:
|
|
return MGLApplicationStateUnknown;
|
|
}
|
|
}
|
|
|
|
- (NSInteger)contentSizeScale {
|
|
NSInteger result = -9999;
|
|
|
|
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;
|
|
}
|
|
|
|
- (void)addBatteryStateToAttributes:(MGLMutableMapboxEventAttributes *)attributes {
|
|
UIDeviceBatteryState batteryState = [[UIDevice currentDevice] batteryState];
|
|
switch (batteryState) {
|
|
case UIDeviceBatteryStateCharging:
|
|
case UIDeviceBatteryStateFull:
|
|
attributes[MGLEventKeyPluggedIn] = @(YES);
|
|
break;
|
|
case UIDeviceBatteryStateUnplugged:
|
|
attributes[MGLEventKeyPluggedIn] = @(NO);
|
|
break;
|
|
default:
|
|
// do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)addApplicationStateToAttributes:(MGLMutableMapboxEventAttributes *)attributes {
|
|
NSString *currentApplicationState = [self applicationState];
|
|
if (![currentApplicationState isEqualToString:MGLApplicationStateUnknown]) {
|
|
attributes[MGLEventKeyApplicationState] = currentApplicationState;
|
|
}
|
|
}
|
|
|
|
+ (void)ensureMetricsOptoutExists {
|
|
NSNumber *shownInAppNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxMetricsEnabledSettingShownInApp"];
|
|
BOOL metricsEnabledSettingShownInAppFlag = [shownInAppNumber boolValue];
|
|
|
|
if (!metricsEnabledSettingShownInAppFlag &&
|
|
[[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0) {
|
|
// Opt-out is not configured in UI, so check for Settings.bundle
|
|
id defaultEnabledValue;
|
|
NSString *appSettingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
|
|
|
|
if (appSettingsBundle) {
|
|
// Dynamic Settings.bundle loading based on http://stackoverflow.com/a/510329/2094275
|
|
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:@"Telemetry opt-out missing" format:
|
|
@"End users must be able to opt out of Mapbox Telemetry in your app, either inside Settings (via Settings.bundle) or inside this app. "
|
|
@"By default, this opt-out control is included as a menu item in the attribution action sheet. "
|
|
@"If you reimplement the opt-out control inside this app, disable this assertion by setting MGLMapboxMetricsEnabledSettingShownInApp to YES in Info.plist."
|
|
@"\n\nSee https://www.mapbox.com/ios-sdk/#telemetry_opt_out for more information."
|
|
@"\n\nAdditionally, by hiding this attribution control you agree to display the required attribution elsewhere in this app."];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark CLLocationManagerUtilityDelegate
|
|
|
|
- (void)locationManager:(MGLLocationManager *)locationManager didUpdateLocations:(NSArray *)locations {
|
|
for (CLLocation *loc in locations) {
|
|
double accuracy = 10000000;
|
|
double lat = floor(loc.coordinate.latitude * accuracy) / accuracy;
|
|
double lng = floor(loc.coordinate.longitude * accuracy) / accuracy;
|
|
NSString *formattedDate = [self.rfc3339DateFormatter stringFromDate:loc.timestamp];
|
|
[MGLMapboxEvents pushEvent:MGLEventTypeLocation withAttributes:@{MGLEventKeyCreated: formattedDate,
|
|
MGLEventKeyLatitude: @(lat),
|
|
MGLEventKeyLongitude: @(lng),
|
|
MGLEventKeyAltitude: @(round(loc.altitude))}];
|
|
}
|
|
}
|
|
|
|
- (void)locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:(MGLLocationManager *)locationManager {
|
|
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerAutoPause"}];
|
|
}
|
|
|
|
- (void)locationManagerBackgroundLocationUpdatesDidTimeout:(MGLLocationManager *)locationManager {
|
|
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerTimeout"}];
|
|
}
|
|
|
|
- (void)locationManagerDidStartLocationUpdates:(MGLLocationManager *)locationManager {
|
|
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerStartUpdates"}];
|
|
}
|
|
|
|
- (void)locationManagerDidStopLocationUpdates:(MGLLocationManager *)locationManager {
|
|
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"locationManager.locationManagerStopUpdates"}];
|
|
}
|
|
|
|
#pragma mark MGLMapboxEvents Debug
|
|
|
|
- (void)pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
if (![self debugLoggingEnabled]) {
|
|
return;
|
|
}
|
|
|
|
if (!event) {
|
|
return;
|
|
}
|
|
|
|
MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:attributeDictionary];
|
|
[evt setObject:event forKey:@"event"];
|
|
[evt setObject:[self.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"];
|
|
[evt setValue:[self applicationState] forKey:@"applicationState"];
|
|
[evt setValue:@([[self class] isEnabled]) forKey:@"telemetryEnabled"];
|
|
[evt setObject:self.instanceID forKey:@"instance"];
|
|
|
|
MGLMapboxEventAttributes *finalEvent = [NSDictionary dictionaryWithDictionary:evt];
|
|
[self writeEventToLocalDebugLog:finalEvent];
|
|
}
|
|
|
|
- (void)writeEventToLocalDebugLog:(MGLMapboxEventAttributes *)event {
|
|
if (![self debugLoggingEnabled]) {
|
|
return;
|
|
}
|
|
|
|
NSLog(@"%@", [self stringForDebugEvent:event]);
|
|
|
|
if (!self.dateForDebugLogFile) {
|
|
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
|
[dateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
|
|
[dateFormatter setTimeZone:[NSTimeZone systemTimeZone]];
|
|
self.dateForDebugLogFile = [dateFormatter stringFromDate:[NSDate date]];
|
|
}
|
|
|
|
if (!self.debugLogSerialQueue) {
|
|
NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
|
|
self.debugLogSerialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.debugLog", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
|
|
}
|
|
|
|
dispatch_async(self.debugLogSerialQueue, ^{
|
|
if ([NSJSONSerialization isValidJSONObject:event]) {
|
|
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:nil];
|
|
|
|
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
jsonString = [jsonString stringByAppendingString:@",\n"];
|
|
|
|
NSString *logFilePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"telemetry_log-%@.json", self.dateForDebugLogFile]];
|
|
|
|
NSFileManager *fileManager = [[NSFileManager alloc] init];
|
|
if ([fileManager fileExistsAtPath:logFilePath]) {
|
|
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
|
|
[fileHandle seekToEndOfFile];
|
|
[fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
|
|
} else {
|
|
[fileManager createFileAtPath:logFilePath contents:[jsonString dataUsingEncoding:NSUTF8StringEncoding] attributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
- (NSString *)stringForDebugEvent:(MGLMapboxEventAttributes *)event {
|
|
// redact potentially sensitive location details from system console log
|
|
if ([event[@"event"] isEqualToString:MGLEventTypeLocation]) {
|
|
MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:event];
|
|
[evt setObject:@"<redacted>" forKey:@"lat"];
|
|
[evt setObject:@"<redacted>" forKey:@"lng"];
|
|
event = evt;
|
|
}
|
|
|
|
return [NSString stringWithFormat:@"Mapbox Telemetry event %@", event];
|
|
}
|
|
|
|
- (BOOL)isProbablyAppStoreBuild {
|
|
#if TARGET_IPHONE_SIMULATOR
|
|
return NO;
|
|
#else
|
|
// BugshotKit by Marco Arment https://github.com/marcoarment/BugshotKit/
|
|
// Adapted from https://github.com/blindsightcorp/BSMobileProvision
|
|
|
|
NSString *binaryMobileProvision = [NSString stringWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"embedded" ofType:@"mobileprovision"] encoding:NSISOLatin1StringEncoding error:NULL];
|
|
if (!binaryMobileProvision) {
|
|
return YES; // no provision
|
|
}
|
|
|
|
NSScanner *scanner = [NSScanner scannerWithString:binaryMobileProvision];
|
|
NSString *plistString;
|
|
if (![scanner scanUpToString:@"<plist" intoString:nil] || ! [scanner scanUpToString:@"</plist>" intoString:&plistString]) {
|
|
return YES; // no XML plist found in provision
|
|
}
|
|
plistString = [plistString stringByAppendingString:@"</plist>"];
|
|
|
|
NSData *plistdata_latin1 = [plistString dataUsingEncoding:NSISOLatin1StringEncoding];
|
|
NSError *error = nil;
|
|
NSDictionary *mobileProvision = [NSPropertyListSerialization propertyListWithData:plistdata_latin1 options:NSPropertyListImmutable format:NULL error:&error];
|
|
if (error) {
|
|
return YES; // unknown plist format
|
|
}
|
|
|
|
if (!mobileProvision || ! mobileProvision.count) {
|
|
return YES; // no entitlements
|
|
}
|
|
|
|
if (mobileProvision[@"ProvisionsAllDevices"]) {
|
|
return NO; // enterprise provisioning
|
|
}
|
|
|
|
if (mobileProvision[@"ProvisionedDevices"] && [mobileProvision[@"ProvisionedDevices"] count]) {
|
|
return NO; // development or ad-hoc
|
|
}
|
|
|
|
return YES; // expected development/enterprise/ad-hoc entitlements not found
|
|
#endif
|
|
}
|
|
|
|
@end
|