Files
MapLibreParticleLab/platform/ios/src/MGLMapboxEvents.m
Jesse Bounds 09c9100891 Introduce MGLLocation Manager
https://github.com/mapbox/mapbox-gl-native/pull/4115

This change makes background data gathering more efficient by disabling
standard location updates when the device has been stationary for at
least five minutes. It also establishes region monitoring and
significant location change monitoring so that if the device appears to
be in motion again then background telemetry data collection can resume.
All of this reduces the amount of time required for telemetry data
collection to the time the device is in motion only. It also only
applies to host apps that already run in the background and have the
always location permission from their users.

This also includes some changes to make the internal pause/resume API of
the MGLMapboxEvents class less complex and autonomous. The side effects
of the map view waking or sleeping are no longer required for mapbox
events to work as intended.
2016-02-26 14:19:04 -08:00

691 lines
28 KiB
Objective-C

#import "MGLMapboxEvents.h"
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
#import "MGLAccountManager.h"
#import "NSProcessInfo+MGLAdditions.h"
#import "NSBundle+MGLAdditions.h"
#import "NSException+MGLAdditions.h"
#import "MGLAPIClient.h"
#import "MGLLocationManager.h"
#include <mbgl/platform/darwin/reachability.h>
#include <sys/sysctl.h>
static const NSUInteger version = 1;
NSString *const MGLEventTypeAppUserTurnstile = @"appUserTurnstile";
NSString *const MGLEventTypeMapLoad = @"map.load";
NSString *const MGLEventTypeMapTap = @"map.click";
NSString *const MGLEventTypeMapDragEnd = @"map.dragend";
NSString *const MGLEventTypeLocation = @"location";
NSString *const MGLEventTypeVisit = @"visit";
NSString *const MGLEventTypeLocalDebug = @"debug";
NSString *const MGLEventKeyLatitude = @"lat";
NSString *const MGLEventKeyLongitude = @"lng";
NSString *const MGLEventKeyZoomLevel = @"zoom";
NSString *const MGLEventKeySpeed = @"speed";
NSString *const MGLEventKeyCourse = @"course";
NSString *const MGLEventKeyAltitude = @"altitude";
NSString *const MGLEventKeyHorizontalAccuracy = @"horizontalAccuracy";
NSString *const MGLEventKeyVerticalAccuracy = @"verticalAccuracy";
NSString *const MGLEventKeyPushEnabled = @"enabled.push";
NSString *const MGLEventKeyEmailEnabled = @"enabled.email";
NSString *const MGLEventKeyGestureID = @"gesture";
NSString *const MGLEventKeyArrivalDate = @"arrivalDate";
NSString *const MGLEventKeyDepartureDate = @"departureDate";
NSString *const MGLEventKeyLocalDebugDescription = @"debug.description";
NSString *const MGLEventGestureSingleTap = @"SingleTap";
NSString *const MGLEventGestureDoubleTap = @"DoubleTap";
NSString *const MGLEventGestureTwoFingerSingleTap = @"TwoFingerTap";
NSString *const MGLEventGestureQuickZoom = @"QuickZoom";
NSString *const MGLEventGesturePanStart = @"Pan";
NSString *const MGLEventGesturePinchStart = @"Pinch";
NSString *const MGLEventGestureRotateStart = @"Rotation";
NSString *const MGLEventGesturePitchStart = @"Pitch";
const NSUInteger MGLMaximumEventsPerFlush = 20;
const NSTimeInterval MGLFlushInterval = 60;
@interface MGLMapboxEventsData : NSObject
@property (nonatomic) NSString *vendorId;
@property (nonatomic) NSString *model;
@property (nonatomic) NSString *iOSVersion;
@property (nonatomic) NSString *carrier;
@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, copy) 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;
@end
@implementation MGLMapboxEvents {
id _userDefaultsObserver;
}
+ (void)initialize {
if (self == [MGLMapboxEvents class]) {
NSBundle *bundle = [NSBundle mainBundle];
NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:@"MGLMapboxAccountType"];
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
@"MGLMapboxAccountType": accountTypeNumber ? accountTypeNumber : @0,
@"MGLMapboxMetricsEnabled": @YES,
@"MGLMapboxMetricsDebugLoggingEnabled": @NO,
}];
}
}
+ (BOOL)isEnabled {
return ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsEnabled"] &&
[[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0);
}
- (BOOL)debugLoggingEnabled {
return (self.canEnableDebugLogging &&
[[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]);
}
- (instancetype) init {
self = [super init];
if (self) {
_appBundleId = [[NSBundle mainBundle] bundleIdentifier];
_instanceID = [[NSUUID UUID] UUIDString];
_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];
NSBundle *resourceBundle = [NSBundle mgl_frameworkBundle];
// 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
__weak MGLMapboxEvents *weakSelf = self;
_userDefaultsObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:
^(NSNotification *notification) {
MGLMapboxEvents *strongSelf = weakSelf;
[strongSelf pauseOrResumeMetricsCollectionIfRequired];
}];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationWillEnterForegroundNotification 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:_userDefaultsObserver];
[self pauseMetricsCollection];
}
- (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 and current settings state
// Practically, a pause only occurs because of a change to an NSUserDefaultsDidChangeNotification
BOOL enabledInSettings = [[self class] isEnabled];
if (self.paused && enabledInSettings) {
[self resumeMetricsCollection];
} else if (!self.paused && !enabledInSettings) {
[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)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 isPaused]) {
return;
}
NSDictionary *stockAttributes = @{@"event": event,
@"version": @(version),
@"created": [self.rfc3339DateFormatter stringFromDate:[NSDate date]],
@"instance": self.instanceID,
@"vendorId": self.data.vendorId,
@"appBundleId": self.appBundleId,
@"model": self.data.model,
@"operatingSystem": self.data.iOSVersion,
@"resolution": @(self.data.scale),
@"wifi": @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi]),
@"orientation": [self deviceOrientation],
@"batteryLevel": @([self batteryLevel]),
@"applicationState": [self applicationState],
@"accessibilityFontScale": @([self contentSizeScale])};
MGLMutableMapboxEventAttributes *allEventAttributes = [NSMutableDictionary dictionaryWithDictionary:attributeDictionary];
[allEventAttributes addEntriesFromDictionary:stockAttributes];
MGLMapboxEventAttributes *finalEvent = [allEventAttributes copy];
[self.eventQueue addObject:finalEvent];
[self writeEventToLocalDebugLog:finalEvent];
// 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];
}
}
- (void)pushTurnstileEvent {
__weak MGLMapboxEvents *weakSelf = self;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
MGLMapboxEvents *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
NSString *vendorID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
if (!vendorID) {
return;
}
NSDictionary *turnstileEventAttributes = @{@"event" : MGLEventTypeAppUserTurnstile,
@"created" : [strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]],
@"appBundleId" : strongSelf.appBundleId,
@"vendorId": vendorID,
@"version": @(version),
@"instance": strongSelf.instanceID};
if ([MGLAccountManager accessToken] == nil) {
return;
}
[strongSelf.apiClient postEvent:turnstileEventAttributes completionHandler:^(NSError * _Nullable error) {
if (error) {
[strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error",
@"error": error}];
return;
}
[strongSelf writeEventToLocalDebugLog:turnstileEventAttributes];
}];
});
}
+ (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"}];
}
// Called implicitly from public use of +flush.
//
- (void)postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events {
if ([self isPaused]) {
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: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 {
NSString *result;
switch ([UIApplication sharedApplication].applicationState) {
case UIApplicationStateActive:
result = @"Active";
break;
case UIApplicationStateInactive:
result = @"Inactive";
break;
case UIApplicationStateBackground:
result = @"Background";
break;
default:
result = @"Default - Unknown";
break;
}
return result;
}
- (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;
}
+ (BOOL)checkPushEnabled {
BOOL blockResult;
if ([[UIApplication sharedApplication] respondsToSelector:@selector(isRegisteredForRemoteNotifications)]) {
// iOS 8+
blockResult = [[UIApplication sharedApplication] isRegisteredForRemoteNotifications];
} else {
// iOS 7
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UIRemoteNotificationType types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];
blockResult = (types == UIRemoteNotificationTypeNone) ? NO : YES;
#pragma clang diagnostic pop
}
return blockResult;
}
+ (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) {
[MGLMapboxEvents pushEvent:MGLEventTypeLocation
withAttributes:@{MGLEventKeyLatitude: @(loc.coordinate.latitude),
MGLEventKeyLongitude: @(loc.coordinate.longitude),
MGLEventKeySpeed: @(loc.speed),
MGLEventKeyCourse: @(loc.course),
MGLEventKeyAltitude: @(round(loc.altitude)),
MGLEventKeyHorizontalAccuracy: @(round(loc.horizontalAccuracy)),
MGLEventKeyVerticalAccuracy: @(round(loc.verticalAccuracy))}];
}
}
- (void)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] ||
[event[@"event"] isEqualToString:MGLEventTypeVisit]) {
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