Apps with `whenInUse` location permission will show a blue status bar when they continue to use location services after leaving the foreground. This is worrying and to be avoided, so let's disable telemetry location services in this situation. Fixes #2945
1085 lines
41 KiB
Objective-C
1085 lines
41 KiB
Objective-C
#import "MGLMapboxEvents.h"
|
|
|
|
#import <UIKit/UIKit.h>
|
|
#import <CoreLocation/CoreLocation.h>
|
|
|
|
#include <mbgl/platform/darwin/reachability.h>
|
|
|
|
#import "MGLAccountManager.h"
|
|
#import "NSProcessInfo+MGLAdditions.h"
|
|
#import "NSBundle+MGLAdditions.h"
|
|
#import "NSException+MGLAdditions.h"
|
|
|
|
#include <sys/sysctl.h>
|
|
|
|
static const NSUInteger version = 1;
|
|
static NSString *const MGLMapboxEventsUserAgent = @"MapboxEventsiOS/1.1";
|
|
static NSString *MGLMapboxEventsAPIBase = @"https://api.tiles.mapbox.com";
|
|
|
|
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
|
|
|
|
// All of the following properties are written to only from
|
|
// the main thread, but can be read on any thread.
|
|
//
|
|
@property (atomic) NSString *vendorId;
|
|
@property (atomic) NSString *model;
|
|
@property (atomic) NSString *iOSVersion;
|
|
@property (atomic) NSString *carrier;
|
|
@property (atomic) 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;
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
- (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
|
|
|
|
//
|
|
// 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 `strongSelf` to avoid cyclical
|
|
// strong references.
|
|
//
|
|
|
|
@interface MGLMapboxEvents () <CLLocationManagerDelegate>
|
|
|
|
// All of the following properties are written to only from
|
|
// the main thread, but can be read on any thread.
|
|
//
|
|
@property (atomic) MGLMapboxEventsData *data;
|
|
@property (atomic) NSString *appBundleId;
|
|
@property (atomic) NSString *appName;
|
|
@property (atomic) NSString *appVersion;
|
|
@property (atomic) NSString *appBuildNumber;
|
|
@property (atomic) NSString *instanceID;
|
|
@property (atomic) NSDateFormatter *rfc3339DateFormatter;
|
|
@property (atomic) NSURLSession *session;
|
|
@property (atomic) NSData *digicertCert;
|
|
@property (atomic) NSData *geoTrustCert;
|
|
@property (atomic) NSData *testServerCert;
|
|
@property (atomic) BOOL usesTestServer;
|
|
|
|
// Main thread only
|
|
@property (nonatomic) CLLocationManager *locationManager;
|
|
|
|
// The paused state tracker is only ever accessed from the main thread.
|
|
//
|
|
@property (nonatomic, getter=isPaused) BOOL paused;
|
|
|
|
// 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) NS_MUTABLE_ARRAY_OF(MGLMapboxEventAttributes *) *eventQueue;
|
|
|
|
// This is a custom serial queue for accessing the event queue.
|
|
//
|
|
@property (nonatomic) dispatch_queue_t serialQueue;
|
|
|
|
@property (atomic) BOOL canEnableDebugLogging;
|
|
@property (nonatomic) dispatch_queue_t debugLogSerialQueue;
|
|
@property (nonatomic) NSString *dateForDebugLogFile;
|
|
|
|
@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"]);
|
|
}
|
|
|
|
+ (BOOL)debugLoggingEnabled {
|
|
return [[MGLMapboxEvents sharedManager] debugLoggingEnabled];
|
|
}
|
|
|
|
// Must be called from the main thread. Only called internally.
|
|
//
|
|
- (instancetype) init {
|
|
MGLAssertIsMainThread();
|
|
|
|
self = [super init];
|
|
if (self) {
|
|
_appBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
|
_appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
|
_appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
|
_appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
|
_instanceID = [[NSUUID UUID] UUIDString];
|
|
|
|
NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
|
|
_serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
|
|
|
|
// Configure Events Infrastructure
|
|
// ===============================
|
|
|
|
// Check for TEST Metrics URL
|
|
NSString *testURL = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMetricsTestServerURL"];
|
|
if (testURL != nil) {
|
|
MGLMapboxEventsAPIBase = testURL;
|
|
_usesTestServer = YES;
|
|
} else {
|
|
// Explicitly Set For Clarity
|
|
_usesTestServer = NO;
|
|
}
|
|
|
|
_paused = YES;
|
|
[self resumeMetricsCollection];
|
|
NSBundle *resourceBundle = [NSBundle mgl_frameworkBundle];
|
|
|
|
// Load Local Copy of Server's Public Key
|
|
NSString *cerPath = nil;
|
|
cerPath = [resourceBundle pathForResource:@"api_mapbox_com-geotrust" ofType:@"der"];
|
|
if (cerPath != nil) {
|
|
_geoTrustCert = [NSData dataWithContentsOfFile:cerPath];
|
|
}
|
|
|
|
cerPath = [resourceBundle pathForResource:@"api_mapbox_com-digicert" ofType:@"der"];
|
|
if (cerPath != nil) {
|
|
_digicertCert = [NSData dataWithContentsOfFile:cerPath];
|
|
}
|
|
cerPath = [resourceBundle pathForResource:@"star_tilestream_net" ofType:@"der"];
|
|
if (cerPath != nil) {
|
|
_testServerCert = [NSData dataWithContentsOfFile:cerPath];
|
|
}
|
|
|
|
// 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 validate];
|
|
}];
|
|
|
|
// Turn the Mapbox Turnstile to Count App Users
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
|
[self pushTurnstileEvent];
|
|
});
|
|
}
|
|
return self;
|
|
}
|
|
|
|
// Can be called from any thread. 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;
|
|
void (^setupBlock)() = ^{
|
|
dispatch_once(&onceToken, ^{
|
|
_sharedManager = [[self alloc] init];
|
|
});
|
|
};
|
|
if ( ! [[NSThread currentThread] isMainThread]) {
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
setupBlock();
|
|
});
|
|
}
|
|
else {
|
|
setupBlock();
|
|
}
|
|
return _sharedManager;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:_userDefaultsObserver];
|
|
[self pauseMetricsCollection];
|
|
}
|
|
|
|
+ (void)validate {
|
|
[[MGLMapboxEvents sharedManager] validate];
|
|
}
|
|
|
|
// Used to determine if Mapbox Metrics should be collected at any given point in time
|
|
- (void)validate {
|
|
MGLAssertIsMainThread();
|
|
BOOL enabledInSettings = [[self class] isEnabled];
|
|
if (self.paused && enabledInSettings) {
|
|
[self resumeMetricsCollection];
|
|
} else if (!self.paused && !enabledInSettings) {
|
|
[self pauseMetricsCollection];
|
|
}
|
|
|
|
[self validateUpdatingLocation];
|
|
}
|
|
|
|
- (void)validateUpdatingLocation {
|
|
MGLAssertIsMainThread();
|
|
if (self.paused) {
|
|
[self stopUpdatingLocation];
|
|
} else {
|
|
switch ([CLLocationManager authorizationStatus]) {
|
|
case kCLAuthorizationStatusNotDetermined:
|
|
case kCLAuthorizationStatusRestricted:
|
|
case kCLAuthorizationStatusDenied:
|
|
[self stopUpdatingLocation];
|
|
break;
|
|
case kCLAuthorizationStatusAuthorized:
|
|
// Also handles kCLAuthorizationStatusAuthorizedAlways
|
|
[self startUpdatingLocation];
|
|
break;
|
|
case kCLAuthorizationStatusAuthorizedWhenInUse:
|
|
if (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) {
|
|
// Prevent blue status bar when app is not in foreground
|
|
[self stopUpdatingLocation];
|
|
} else {
|
|
[self startUpdatingLocation];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
+ (void)pauseMetricsCollection {
|
|
[[MGLMapboxEvents sharedManager] pauseMetricsCollection];
|
|
}
|
|
|
|
// Must be called from the main thread.
|
|
//
|
|
- (void)pauseMetricsCollection {
|
|
MGLAssertIsMainThread();
|
|
if (self.paused) {
|
|
return;
|
|
}
|
|
self.paused = YES;
|
|
[_timer invalidate];
|
|
_timer = nil;
|
|
[_eventQueue removeAllObjects];
|
|
_data = nil;
|
|
[_session invalidateAndCancel];
|
|
_session = nil;
|
|
|
|
[self validateUpdatingLocation];
|
|
}
|
|
|
|
- (void)stopUpdatingLocation {
|
|
[_locationManager stopUpdatingLocation];
|
|
|
|
// -[CLLocationManager stopMonitoringVisits] is only available in iOS 8+.
|
|
if ([_locationManager respondsToSelector:@selector(stopMonitoringVisits)]) {
|
|
[_locationManager stopMonitoringVisits];
|
|
}
|
|
|
|
_locationManager = nil;
|
|
}
|
|
|
|
+ (void)resumeMetricsCollection {
|
|
[[MGLMapboxEvents sharedManager] resumeMetricsCollection];
|
|
}
|
|
|
|
// Must be called from the main thread.
|
|
//
|
|
- (void)resumeMetricsCollection {
|
|
MGLAssertIsMainThread();
|
|
if (!self.paused || ![[self class] isEnabled]) {
|
|
return;
|
|
}
|
|
self.paused = NO;
|
|
_data = [[MGLMapboxEventsData alloc] init];
|
|
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
|
|
|
|
[self validateUpdatingLocation];
|
|
}
|
|
|
|
- (void)startUpdatingLocation {
|
|
MGLAssertIsMainThread();
|
|
if (_locationManager || _paused) {
|
|
NSAssert(!(_locationManager && _paused),
|
|
@"MGLMapboxEvents should not have a CLLocationManager while paused.");
|
|
return;
|
|
}
|
|
_locationManager = [[CLLocationManager alloc] init];
|
|
_locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
|
|
_locationManager.distanceFilter = 10;
|
|
_locationManager.delegate = self;
|
|
|
|
[_locationManager startUpdatingLocation];
|
|
|
|
// -[CLLocationManager startMonitoringVisits] is only available in iOS 8+.
|
|
if ([_locationManager respondsToSelector:@selector(startMonitoringVisits)]) {
|
|
[_locationManager startMonitoringVisits];
|
|
}
|
|
}
|
|
|
|
- (void) pushTurnstileEvent {
|
|
|
|
__weak MGLMapboxEvents *weakSelf = self;
|
|
|
|
dispatch_async(_serialQueue, ^{
|
|
|
|
MGLMapboxEvents *strongSelf = weakSelf;
|
|
|
|
if ( ! strongSelf) return;
|
|
|
|
// Build only IDFV event
|
|
NSString *vid = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
|
|
|
|
if (!vid) return;
|
|
|
|
NSDictionary *vevt = @{
|
|
@"event" : MGLEventTypeAppUserTurnstile,
|
|
@"created" : [strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]],
|
|
@"appBundleId" : strongSelf.appBundleId,
|
|
@"vendorId": vid,
|
|
@"version": @(version),
|
|
@"instance": strongSelf.instanceID
|
|
};
|
|
|
|
// Add to Queue
|
|
[_eventQueue addObject:vevt];
|
|
|
|
// Flush
|
|
[strongSelf flush];
|
|
|
|
if ([strongSelf debugLoggingEnabled]) {
|
|
[strongSelf writeEventToLocalDebugLog:vevt];
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
// Can be called from any thread. Can be called rapidly from
|
|
// the UI thread, so performance is paramount.
|
|
//
|
|
+ (void) pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)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:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
__weak MGLMapboxEvents *weakSelf = self;
|
|
|
|
dispatch_async(_serialQueue, ^{
|
|
|
|
MGLMapboxEvents *strongSelf = weakSelf;
|
|
|
|
if ( ! strongSelf) return;
|
|
|
|
// Metrics Collection Has Been Paused
|
|
if (_paused) {
|
|
return;
|
|
}
|
|
|
|
if (!event) return;
|
|
|
|
MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:attributeDictionary];
|
|
// mapbox-events stock attributes
|
|
[evt setObject:event forKey:@"event"];
|
|
[evt setObject:@(version) forKey:@"version"];
|
|
[evt setObject:[strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"];
|
|
[evt setObject:strongSelf.instanceID forKey:@"instance"];
|
|
[evt setObject:strongSelf.data.vendorId forKey:@"vendorId"];
|
|
[evt setObject:strongSelf.appBundleId forKeyedSubscript:@"appBundleId"];
|
|
|
|
// mapbox-events-ios stock attributes
|
|
[evt setValue:strongSelf.data.model forKey:@"model"];
|
|
[evt setValue:strongSelf.data.iOSVersion forKey:@"operatingSystem"];
|
|
[evt setValue:[strongSelf deviceOrientation] forKey:@"orientation"];
|
|
[evt setValue:@((int)(100 * [UIDevice currentDevice].batteryLevel)) forKey:@"batteryLevel"];
|
|
[evt setValue:@(strongSelf.data.scale) forKey:@"resolution"];
|
|
|
|
MGLReachability *reachability = [MGLReachability reachabilityForLocalWiFi];
|
|
[evt setValue:([reachability isReachableViaWiFi] ? @YES : @NO) forKey:@"wifi"];
|
|
|
|
[evt setValue:[strongSelf applicationState] forKey:@"applicationState"];
|
|
|
|
[evt setValue:@([strongSelf contentSizeScale]) forKey:@"accessibilityFontScale"];
|
|
|
|
// Make Immutable Version
|
|
NSDictionary *finalEvent = [NSDictionary dictionaryWithDictionary:evt];
|
|
|
|
// Put On The Queue
|
|
[_eventQueue addObject:finalEvent];
|
|
|
|
// Has Flush Limit Been Reached?
|
|
if (_eventQueue.count >= MGLMaximumEventsPerFlush) {
|
|
[strongSelf flush];
|
|
} else if (_eventQueue.count == 1) {
|
|
// If this is first new event on queue start timer,
|
|
[strongSelf startTimer];
|
|
}
|
|
|
|
if ([strongSelf debugLoggingEnabled]) {
|
|
[strongSelf writeEventToLocalDebugLog:finalEvent];
|
|
}
|
|
});
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
+ (void) flush {
|
|
[[MGLMapboxEvents sharedManager] flush];
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
- (void) flush {
|
|
if ([MGLAccountManager accessToken] == nil) return;
|
|
|
|
__weak MGLMapboxEvents *weakSelf = self;
|
|
|
|
dispatch_async(_serialQueue, ^{
|
|
MGLMapboxEvents *strongSelf = weakSelf;
|
|
if ( ! strongSelf || [_eventQueue count] == 0) return;
|
|
|
|
// Make an immutable copy
|
|
NSArray *events = [NSArray arrayWithArray:_eventQueue];
|
|
|
|
// Update Queue to remove events sent to server
|
|
[_eventQueue removeAllObjects];
|
|
|
|
// Send Array of Events to Server
|
|
[strongSelf postEvents:events];
|
|
|
|
// Cancel Any Timer That May Running
|
|
if (strongSelf.timer) {
|
|
[strongSelf.timer invalidate];
|
|
strongSelf.timer = nil;
|
|
}
|
|
});
|
|
|
|
if ([self debugLoggingEnabled]) {
|
|
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{
|
|
MGLEventKeyLocalDebugDescription: @"flush"
|
|
}];
|
|
}
|
|
}
|
|
|
|
// Can be called from any thread. Called implicitly from public
|
|
// use of +flush. Posts an async network request to upload metrics.
|
|
//
|
|
- (void) postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events {
|
|
__weak MGLMapboxEvents *weakSelf = self;
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
|
MGLMapboxEvents *strongSelf = weakSelf;
|
|
if (!strongSelf) return;
|
|
|
|
// Setup URL Request
|
|
NSString *url = [NSString stringWithFormat:@"%@/events/v1?access_token=%@", MGLMapboxEventsAPIBase, [MGLAccountManager accessToken]];
|
|
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
|
|
[request setValue:strongSelf.userAgent 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
|
|
if ( ! strongSelf.paused) {
|
|
[[strongSelf.session dataTaskWithRequest:request] resume];
|
|
} else {
|
|
for (MGLMapboxEventAttributes *event in events) {
|
|
if ([event[@"event"] isEqualToString:MGLEventTypeAppUserTurnstile]) {
|
|
NSURLSession *temporarySession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
|
|
delegate:strongSelf
|
|
delegateQueue:nil];
|
|
[[temporarySession dataTaskWithRequest:request] resume];
|
|
[temporarySession finishTasksAndInvalidate];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ([self debugLoggingEnabled]) {
|
|
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{
|
|
MGLEventKeyLocalDebugDescription: @"post",
|
|
@"debug.eventsCount": @(events.count)
|
|
}];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
- (void) startTimer {
|
|
void (^timerBlock)() = ^{
|
|
// Stop Timer if it already exists
|
|
[_timer invalidate];
|
|
|
|
// Start New Timer
|
|
_timer = [NSTimer scheduledTimerWithTimeInterval:MGLFlushInterval
|
|
target:self
|
|
selector:@selector(flush)
|
|
userInfo:nil
|
|
repeats:YES];
|
|
};
|
|
|
|
if ( ! [[NSThread currentThread] isMainThread]) {
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
timerBlock();
|
|
});
|
|
} else {
|
|
timerBlock();
|
|
}
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
- (NSString *) userAgent {
|
|
return [NSString stringWithFormat:@"%@/%@/%@ %@", self.appName, self.appVersion, self.appBuildNumber, MGLMapboxEventsUserAgent];
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
- (NSString *) deviceOrientation {
|
|
__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;
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
- (NSString *) applicationState {
|
|
__block NSString *result;
|
|
|
|
NSString *(^applicationStateBlock)(void) = ^{
|
|
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;
|
|
};
|
|
|
|
if ( ! [[NSThread currentThread] isMainThread]) {
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
result = applicationStateBlock();
|
|
});
|
|
} else {
|
|
result = applicationStateBlock();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
- (NSInteger) contentSizeScale {
|
|
__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 result;
|
|
}
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
+ (BOOL) checkPushEnabled {
|
|
BOOL (^pushCheckBlock)(void) = ^{
|
|
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;
|
|
};
|
|
|
|
__block BOOL result;
|
|
|
|
if ( ! [[NSThread currentThread] isMainThread]) {
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
result = pushCheckBlock();
|
|
});
|
|
} else {
|
|
result = pushCheckBlock();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Main thread only
|
|
//
|
|
+ (void) ensureMetricsOptoutExists {
|
|
MGLAssertIsMainThread();
|
|
|
|
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 CLLocationManagerDelegate
|
|
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
|
|
// Iterate through locations to pass all data
|
|
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)locationManager:(CLLocationManager *)manager didVisit:(CLVisit *)visit {
|
|
[MGLMapboxEvents pushEvent:MGLEventTypeVisit withAttributes:@{
|
|
MGLEventKeyLatitude: @(visit.coordinate.latitude),
|
|
MGLEventKeyLongitude: @(visit.coordinate.longitude),
|
|
MGLEventKeyHorizontalAccuracy: @(round(visit.horizontalAccuracy)),
|
|
MGLEventKeyArrivalDate: [[NSDate distantPast] isEqualToDate:visit.arrivalDate] ? [NSNull null] : [_rfc3339DateFormatter stringFromDate:visit.arrivalDate],
|
|
MGLEventKeyDepartureDate: [[NSDate distantFuture] isEqualToDate:visit.departureDate] ? [NSNull null] : [_rfc3339DateFormatter stringFromDate:visit.departureDate]
|
|
}];
|
|
}
|
|
|
|
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
|
|
[self validateUpdatingLocation];
|
|
}
|
|
|
|
#pragma mark NSURLSessionDelegate
|
|
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
|
|
|
|
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
|
|
|
|
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
|
|
SecTrustResultType trustResult;
|
|
|
|
// Validate the certificate chain with the device's trust store anyway
|
|
// This *might* give use revocation checking
|
|
SecTrustEvaluate(serverTrust, &trustResult);
|
|
if (trustResult == kSecTrustResultUnspecified)
|
|
{
|
|
// Look for a pinned certificate in the server's certificate chain
|
|
long numKeys = SecTrustGetCertificateCount(serverTrust);
|
|
|
|
BOOL found = NO;
|
|
// Try GeoTrust Cert First
|
|
for (int lc = 0; lc < numKeys; lc++) {
|
|
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc);
|
|
NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
|
|
|
|
// Compare Remote Key With Local Version
|
|
if ([remoteCertificateData isEqualToData:_geoTrustCert]) {
|
|
// Found the certificate; continue connecting
|
|
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
|
|
found = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
// Fallback to Digicert Cert
|
|
for (int lc = 0; lc < numKeys; lc++) {
|
|
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc);
|
|
NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
|
|
|
|
// Compare Remote Key With Local Version
|
|
if ([remoteCertificateData isEqualToData:_digicertCert]) {
|
|
// Found the certificate; continue connecting
|
|
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
|
|
found = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found && _usesTestServer) {
|
|
// See if this is test server
|
|
for (int lc = 0; lc < numKeys; lc++) {
|
|
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc);
|
|
NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
|
|
|
|
// Compare Remote Key With Local Version
|
|
if ([remoteCertificateData isEqualToData:_testServerCert]) {
|
|
// Found the certificate; continue connecting
|
|
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
|
|
found = YES;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
// The certificate wasn't found in GeoTrust nor Digicert. Cancel the connection.
|
|
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Certificate chain validation failed; cancel the connection
|
|
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#pragma mark MGLMapboxEvents Debug
|
|
|
|
// Can be called from any thread.
|
|
//
|
|
+ (void) pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
|
[[MGLMapboxEvents sharedManager] pushDebugEvent:event withAttributes:attributeDictionary];
|
|
});
|
|
}
|
|
|
|
// Can be called from any thread. Called implicitly from public
|
|
// use of +pushDebugEvent:withAttributes:.
|
|
//
|
|
- (void) pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
|
|
__weak MGLMapboxEvents *weakSelf = self;
|
|
|
|
if (![self debugLoggingEnabled] || !event) return;
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
|
|
|
MGLMapboxEvents *strongSelf = weakSelf;
|
|
|
|
if (!strongSelf) return;
|
|
|
|
MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:attributeDictionary];
|
|
|
|
[evt setObject:event forKey:@"event"];
|
|
[evt setObject:[strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"];
|
|
[evt setValue:[strongSelf applicationState] forKey:@"applicationState"];
|
|
[evt setValue:@([[self class] isEnabled]) forKey:@"telemetryEnabled"];
|
|
[evt setObject:strongSelf.instanceID forKey:@"instance"];
|
|
|
|
// Make immutable version
|
|
MGLMapboxEventAttributes *finalEvent = [NSDictionary dictionaryWithDictionary:evt];
|
|
|
|
[strongSelf 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 ( ! _debugLogSerialQueue) {
|
|
NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
|
|
_debugLogSerialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.debugLog", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
|
|
}
|
|
|
|
dispatch_sync(_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
|