diff options
Diffstat (limited to 'platform/ios/src')
-rw-r--r-- | platform/ios/src/MGLAPIClient.h | 15 | ||||
-rw-r--r-- | platform/ios/src/MGLAPIClient.m | 202 | ||||
-rw-r--r-- | platform/ios/src/MGLLocationManager.h | 25 | ||||
-rw-r--r-- | platform/ios/src/MGLLocationManager.m | 175 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 41 | ||||
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.h | 39 | ||||
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.m | 831 | ||||
-rw-r--r-- | platform/ios/src/Mapbox-Prefix.pch | 1 |
8 files changed, 130 insertions, 1199 deletions
diff --git a/platform/ios/src/MGLAPIClient.h b/platform/ios/src/MGLAPIClient.h deleted file mode 100644 index 4e5ea3b5e0..0000000000 --- a/platform/ios/src/MGLAPIClient.h +++ /dev/null @@ -1,15 +0,0 @@ -#import <Foundation/Foundation.h> - -#import "MGLMapboxEvents.h" -#import "MGLTypes.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface MGLAPIClient : NSObject <NSURLSessionDelegate> - -- (void)postEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler; -- (void)postEvent:(MGLMapboxEventAttributes *)event completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLAPIClient.m b/platform/ios/src/MGLAPIClient.m deleted file mode 100644 index 8a987d76d8..0000000000 --- a/platform/ios/src/MGLAPIClient.m +++ /dev/null @@ -1,202 +0,0 @@ -#import "MGLAPIClient.h" -#import "NSBundle+MGLAdditions.h" -#import "NSData+MGLAdditions.h" -#import "MGLAccountManager.h" - -static NSString * const MGLAPIClientUserAgentBase = @"MapboxEventsiOS"; -static NSString * const MGLAPIClientBaseURL = @"https://events.mapbox.com"; -static NSString * const MGLAPIClientEventsPath = @"events/v2"; - -static NSString * const MGLAPIClientHeaderFieldUserAgentKey = @"User-Agent"; -static NSString * const MGLAPIClientHeaderFieldContentTypeKey = @"Content-Type"; -static NSString * const MGLAPIClientHeaderFieldContentTypeValue = @"application/json"; -static NSString * const MGLAPIClientHeaderFieldContentEncodingKey = @"Content-Encoding"; -static NSString * const MGLAPIClientHTTPMethodPost = @"POST"; - -@interface MGLAPIClient () - -@property (nonatomic, copy) NSURLSession *session; -@property (nonatomic, copy) NSURL *baseURL; -@property (nonatomic, copy) NSData *digicertCert_2016; -@property (nonatomic, copy) NSData *geoTrustCert_2016; -@property (nonatomic, copy) NSData *digicertCert_2017; -@property (nonatomic, copy) NSData *geoTrustCert_2017; -@property (nonatomic, copy) NSData *testServerCert; -@property (nonatomic, copy) NSString *userAgent; -@property (nonatomic) BOOL usesTestServer; - -@end - -@implementation MGLAPIClient - -- (instancetype)init { - self = [super init]; - if (self) { - _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] - delegate:self delegateQueue:nil]; - [self loadCertificates]; - [self setupBaseURL]; - [self setupUserAgent]; - } - return self; -} - -#pragma mark Public API - -- (void)postEvents:(nonnull NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler { - __block NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:[self requestForEvents:events] - completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - NSError *statusError = nil; - if (httpResponse.statusCode >= 400) { - NSString *description = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"API_CLIENT_400_DESC", nil, nil, @"The session data task failed. Original request was: %@", nil), dataTask.originalRequest]; - NSString *reason = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"API_CLIENT_400_REASON", nil, nil, @"The status code was %ld", nil), (long)httpResponse.statusCode]; - NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description, - NSLocalizedFailureReasonErrorKey: reason}; - statusError = [NSError errorWithDomain:MGLErrorDomain code:1 userInfo:userInfo]; - } - if (completionHandler) { - error = error ?: statusError; - completionHandler(error); - } - dataTask = nil; - }]; - [dataTask resume]; -} - -- (void)postEvent:(nonnull MGLMapboxEventAttributes *)event completionHandler:(nullable void (^)(NSError * _Nullable error))completionHandler { - [self postEvents:@[event] completionHandler:completionHandler]; -} - -#pragma mark Utilities - -- (NSURLRequest *)requestForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { - NSString *path = [NSString stringWithFormat:@"%@?access_token=%@", MGLAPIClientEventsPath, [MGLAccountManager accessToken]]; - NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseURL]; - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - [request setValue:self.userAgent forHTTPHeaderField:MGLAPIClientHeaderFieldUserAgentKey]; - [request setValue:MGLAPIClientHeaderFieldContentTypeValue forHTTPHeaderField:MGLAPIClientHeaderFieldContentTypeKey]; - [request setHTTPMethod:MGLAPIClientHTTPMethodPost]; - - NSData *jsonData = [self serializedDataForEvents:events]; - - // Compressing less than 3 events can have a negative impact on the size. - if (events.count > 2) { - NSData *compressedData = [jsonData mgl_compressedData]; - [request setValue:@"deflate" forHTTPHeaderField:MGLAPIClientHeaderFieldContentEncodingKey]; - [request setHTTPBody:compressedData]; - } - - // Set JSON data if events.count were less than 3 or something went wrong with compressing HTTP body data. - if (!request.HTTPBody) { - [request setValue:nil forHTTPHeaderField:MGLAPIClientHeaderFieldContentEncodingKey]; - [request setHTTPBody:jsonData]; - } - - return [request copy]; -} - -- (void)setupBaseURL { - NSString *testServerURLString = [[NSUserDefaults standardUserDefaults] stringForKey:@"MGLTelemetryTestServerURL"]; - NSURL *testServerURL = [NSURL URLWithString:testServerURLString]; - if (testServerURL && [testServerURL.scheme isEqualToString:@"https"]) { - self.baseURL = testServerURL; - self.usesTestServer = YES; - } else { - self.baseURL = [NSURL URLWithString:MGLAPIClientBaseURL]; - } -} - -- (void)loadCertificates { - NSData *certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-geotrust_2016"]; - self.geoTrustCert_2016 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-digicert_2016"]; - self.digicertCert_2016 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-geotrust_2017"]; - self.geoTrustCert_2017 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_com-digicert_2017"]; - self.digicertCert_2017 = certificate; - [self loadCertificate:&certificate withResource:@"api_mapbox_staging"]; - self.testServerCert = certificate; -} - -- (void)loadCertificate:(NSData **)certificate withResource:(NSString *)resource { - NSBundle *frameworkBundle = [NSBundle mgl_frameworkBundle]; - NSString *cerPath = [frameworkBundle pathForResource:resource ofType:@"der"]; - if (cerPath != nil) { - *certificate = [NSData dataWithContentsOfFile:cerPath]; - } -} - -- (void)setupUserAgent { - NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; - NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - NSString *appBuildNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; - NSString *semanticVersion = [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"]; - NSString *shortVersion = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"]; - NSString *sdkVersion = semanticVersion ?: shortVersion; - _userAgent = [NSString stringWithFormat:@"%@/%@/%@ %@/%@", appName, appVersion, appBuildNumber, MGLAPIClientUserAgentBase, sdkVersion]; -} - -#pragma mark - JSON Serialization - -- (NSData *)serializedDataForEvents:(NS_ARRAY_OF(MGLMapboxEventAttributes *) *)events { - return [NSJSONSerialization dataWithJSONObject:events options:0 error:nil]; -} - -#pragma mark NSURLSessionDelegate - -- (BOOL)evaluateCertificateWithCertificateData:(NSData *)certificateData keyCount:(CFIndex)keyCount serverTrust:(SecTrustRef)serverTrust challenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^) (NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { - for (int lc = 0; lc < keyCount; lc++) { - SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, lc); - NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate)); - if ([remoteCertificateData isEqualToData:certificateData]) { - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - return YES; - } - } - return NO; -} - -- (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* use revocation checking - SecTrustEvaluate(serverTrust, &trustResult); - - BOOL found = NO; // For clarity; we start in a state where the challange has not been completed and no certificate has been found - - if (trustResult == kSecTrustResultUnspecified) { - // Look for a pinned certificate in the server's certificate chain - CFIndex numKeys = SecTrustGetCertificateCount(serverTrust); - - // Check certs in the following order: digicert 2016, digicert 2017, geotrust 2016, geotrust 2017 - found = [self evaluateCertificateWithCertificateData:self.digicertCert_2016 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - if (!found) { - found = [self evaluateCertificateWithCertificateData:self.digicertCert_2017 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - if (!found) { - found = [self evaluateCertificateWithCertificateData:self.geoTrustCert_2016 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - if (!found) { - found = [self evaluateCertificateWithCertificateData:self.geoTrustCert_2017 keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - - // If challenge can't be completed with any of the above certs, then try the test server if the app is configured to use the test server - if (!found && _usesTestServer) { - found = [self evaluateCertificateWithCertificateData:self.testServerCert keyCount:numKeys serverTrust:serverTrust challenge:challenge completionHandler:completionHandler]; - } - } - - if (!found) { - // No certificate was found so cancel the connection. - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - } - } -} - -@end diff --git a/platform/ios/src/MGLLocationManager.h b/platform/ios/src/MGLLocationManager.h deleted file mode 100644 index ea23801813..0000000000 --- a/platform/ios/src/MGLLocationManager.h +++ /dev/null @@ -1,25 +0,0 @@ -#import <Foundation/Foundation.h> -#import <CoreLocation/CoreLocation.h> - -@protocol MGLLocationManagerDelegate; - -@interface MGLLocationManager : NSObject <CLLocationManagerDelegate> - -@property (nonatomic, weak) id<MGLLocationManagerDelegate> delegate; - -- (void)startUpdatingLocation; -- (void)stopUpdatingLocation; - -@end - -@protocol MGLLocationManagerDelegate <NSObject> - -@optional - -- (void)locationManager:(MGLLocationManager *)locationManager didUpdateLocations:(NSArray *)locations; -- (void)locationManagerDidStartLocationUpdates:(MGLLocationManager *)locationManager; -- (void)locationManagerBackgroundLocationUpdatesDidTimeout:(MGLLocationManager *)locationManager; -- (void)locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:(MGLLocationManager *)locationManager; -- (void)locationManagerDidStopLocationUpdates:(MGLLocationManager *)locationManager; - -@end diff --git a/platform/ios/src/MGLLocationManager.m b/platform/ios/src/MGLLocationManager.m deleted file mode 100644 index 85ef4ca489..0000000000 --- a/platform/ios/src/MGLLocationManager.m +++ /dev/null @@ -1,175 +0,0 @@ -#import "MGLLocationManager.h" -#import "MGLTelemetryConfig.h" -#import <UIKit/UIKit.h> - -static const NSTimeInterval MGLLocationManagerHibernationTimeout = 300.0; -static const NSTimeInterval MGLLocationManagerHibernationPollInterval = 5.0; -static const CLLocationDistance MGLLocationManagerDistanceFilter = 5.0; -static NSString * const MGLLocationManagerRegionIdentifier = @"MGLLocationManagerRegionIdentifier.fence.center"; - -@interface MGLLocationManager () - -@property (nonatomic) CLLocationManager *standardLocationManager; -@property (nonatomic) BOOL hostAppHasBackgroundCapability; -@property (nonatomic, getter=isUpdatingLocation) BOOL updatingLocation; -@property (nonatomic) NSDate *backgroundLocationServiceTimeoutAllowedDate; -@property (nonatomic) NSTimer *backgroundLocationServiceTimeoutTimer; - -@end - -@implementation MGLLocationManager - -- (instancetype)init { - self = [super init]; - if (self) { - NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; - _hostAppHasBackgroundCapability = [backgroundModes containsObject:@"location"]; - } - return self; -} - -- (void)startUpdatingLocation { - if ([self isUpdatingLocation]) { - return; - } - - [self configurePassiveStandardLocationManager]; - [self startLocationServices]; -} - -- (void)stopUpdatingLocation { - if ([self isUpdatingLocation]) { - [self.standardLocationManager stopUpdatingLocation]; - [self.standardLocationManager stopMonitoringSignificantLocationChanges]; - self.updatingLocation = NO; - if ([self.delegate respondsToSelector:@selector(locationManagerDidStopLocationUpdates:)]) { - [self.delegate locationManagerDidStopLocationUpdates:self]; - } - } - if(self.standardLocationManager.monitoredRegions.count > 0) { - for(CLRegion *region in self.standardLocationManager.monitoredRegions) { - if([region.identifier isEqualToString:MGLLocationManagerRegionIdentifier]) { - [self.standardLocationManager stopMonitoringForRegion:region]; - } - } - } -} - -#pragma mark - Utilities - -- (void)configurePassiveStandardLocationManager { - if (!self.standardLocationManager) { - CLLocationManager *standardLocationManager = [[CLLocationManager alloc] init]; - standardLocationManager.delegate = self; - standardLocationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers; - standardLocationManager.distanceFilter = MGLLocationManagerDistanceFilter; - self.standardLocationManager = standardLocationManager; - } -} - -- (void)startLocationServices { - CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus]; -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000 - BOOL authorizedAlways = authorizationStatus == kCLAuthorizationStatusAuthorizedAlways; -#else - BOOL authorizedAlways = authorizationStatus == kCLAuthorizationStatusAuthorized; -#endif - if (authorizedAlways || authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse) { - // If the host app can run in the background with `always` location permissions then allow background - // updates and start the significant location change service and background timeout timer - if (self.hostAppHasBackgroundCapability && authorizedAlways) { - [self.standardLocationManager startMonitoringSignificantLocationChanges]; - [self startBackgroundTimeoutTimer]; - // On iOS 9 and above also allow background location updates - if ([self.standardLocationManager respondsToSelector:@selector(allowsBackgroundLocationUpdates)]) { - self.standardLocationManager.allowsBackgroundLocationUpdates = YES; - } - } - - [self.standardLocationManager startUpdatingLocation]; - self.updatingLocation = YES; - if ([self.delegate respondsToSelector:@selector(locationManagerDidStartLocationUpdates:)]) { - [self.delegate locationManagerDidStartLocationUpdates:self]; - } - } -} - -- (void)timeoutAllowedCheck { - if (self.backgroundLocationServiceTimeoutAllowedDate == nil) { - return; - } - - if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive || - [UIApplication sharedApplication].applicationState == UIApplicationStateInactive ) { - [self startBackgroundTimeoutTimer]; - return; - } - - NSTimeInterval timeIntervalSinceTimeoutAllowed = [[NSDate date] timeIntervalSinceDate:self.backgroundLocationServiceTimeoutAllowedDate]; - if (timeIntervalSinceTimeoutAllowed > 0) { - [self.standardLocationManager stopUpdatingLocation]; - self.backgroundLocationServiceTimeoutAllowedDate = nil; - if ([self.delegate respondsToSelector:@selector(locationManagerBackgroundLocationUpdatesDidTimeout:)]) { - [self.delegate locationManagerBackgroundLocationUpdatesDidTimeout:self]; - } - } -} - -- (void)startBackgroundTimeoutTimer { - [self.backgroundLocationServiceTimeoutTimer invalidate]; - self.backgroundLocationServiceTimeoutAllowedDate = [[NSDate date] dateByAddingTimeInterval:MGLLocationManagerHibernationTimeout]; - self.backgroundLocationServiceTimeoutTimer = [NSTimer scheduledTimerWithTimeInterval:MGLLocationManagerHibernationPollInterval target:self selector:@selector(timeoutAllowedCheck) userInfo:nil repeats:YES]; -} - -- (void)establishRegionMonitoringForLocation:(CLLocation *)location { - CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:location.coordinate radius:MGLTelemetryConfig.sharedConfig.MGLLocationManagerHibernationRadius identifier:MGLLocationManagerRegionIdentifier]; - region.notifyOnEntry = NO; - region.notifyOnExit = YES; - [self.standardLocationManager startMonitoringForRegion:region]; -} - -#pragma mark - CLLocationManagerDelegate - -- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { - switch (status) { -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000 - case kCLAuthorizationStatusAuthorizedAlways: -#else - case kCLAuthorizationStatusAuthorized: -#endif - case kCLAuthorizationStatusAuthorizedWhenInUse: - [self startUpdatingLocation]; - break; - default: - [self stopUpdatingLocation]; - break; - } -} - -- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations { - CLLocation *location = locations.lastObject; - if (location.speed > 0.0) { - [self startBackgroundTimeoutTimer]; - } - if (self.standardLocationManager.monitoredRegions.count == 0 || location.horizontalAccuracy < MGLTelemetryConfig.sharedConfig.MGLLocationManagerHibernationRadius) { - [self establishRegionMonitoringForLocation:location]; - } - if ([self.delegate respondsToSelector:@selector(locationManager:didUpdateLocations:)]) { - [self.delegate locationManager:self didUpdateLocations:locations]; - } -} - -- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region { - [self startBackgroundTimeoutTimer]; - [self.standardLocationManager startUpdatingLocation]; -} - -- (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager { - if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { - if ([self.delegate respondsToSelector:@selector(locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:)]) { - [self.delegate locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:self]; - } - } -} - -@end diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 72fa4613fc..927450b511 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -66,6 +66,7 @@ #import "MGLStyle_Private.h" #import "MGLStyleLayer_Private.h" #import "MGLMapboxEvents.h" +#import "MMEConstants.h" #import "MGLSDKUpdateChecker.h" #import "MGLCompactCalloutView.h" #import "MGLAnnotationContainerView.h" @@ -567,7 +568,8 @@ public: _targetCoordinate = kCLLocationCoordinate2DInvalid; if ([UIApplication sharedApplication].applicationState != UIApplicationStateBackground) { - [MGLMapboxEvents pushEvent:MGLEventTypeMapLoad withAttributes:@{}]; + [MGLMapboxEvents pushTurnstileEvent]; + [MGLMapboxEvents pushEvent:MMEEventTypeMapLoad withAttributes:@{}]; } } @@ -1219,7 +1221,8 @@ public: [self validateLocationServices]; - [MGLMapboxEvents pushEvent:MGLEventTypeMapLoad withAttributes:@{}]; + [MGLMapboxEvents pushTurnstileEvent]; + [MGLMapboxEvents pushEvent:MMEEventTypeMapLoad withAttributes:@{}]; } } @@ -1331,7 +1334,7 @@ public: if (pan.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePanStart forRecognizer:pan]; + [self trackGestureEvent:MMEEventGesturePanStart forRecognizer:pan]; self.userTrackingMode = MGLUserTrackingModeNone; @@ -1379,10 +1382,10 @@ public: CLLocationCoordinate2D panCoordinate = [self convertPoint:pointInView toCoordinateFromView:pan.view]; int zoom = round([self zoomLevel]); - [MGLMapboxEvents pushEvent:MGLEventTypeMapDragEnd withAttributes:@{ - MGLEventKeyLatitude: @(panCoordinate.latitude), - MGLEventKeyLongitude: @(panCoordinate.longitude), - MGLEventKeyZoomLevel: @(zoom) + [MGLMapboxEvents pushEvent:MMEEventTypeMapDragEnd withAttributes:@{ + MMEEventKeyLatitude: @(panCoordinate.latitude), + MMEEventKeyLongitude: @(panCoordinate.longitude), + MMEEventKeyZoomLevel: @(zoom) }]; } @@ -1401,7 +1404,7 @@ public: if (pinch.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePinchStart forRecognizer:pinch]; + [self trackGestureEvent:MMEEventGesturePinchStart forRecognizer:pinch]; self.scale = powf(2, _mbglMap->getZoom()); @@ -1500,7 +1503,7 @@ public: if (rotate.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureRotateStart forRecognizer:rotate]; + [self trackGestureEvent:MMEEventGestureRotateStart forRecognizer:rotate]; self.angle = MGLRadiansFromDegrees(_mbglMap->getBearing()) * -1; @@ -1572,7 +1575,7 @@ public: { return; } - [self trackGestureEvent:MGLEventGestureSingleTap forRecognizer:singleTap]; + [self trackGestureEvent:MMEEventGestureSingleTap forRecognizer:singleTap]; if (self.mapViewProxyAccessibilityElement.accessibilityElementIsFocused) { @@ -1694,7 +1697,7 @@ public: if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - [self trackGestureEvent:MGLEventGestureDoubleTap forRecognizer:doubleTap]; + [self trackGestureEvent:MMEEventGestureDoubleTap forRecognizer:doubleTap]; mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y); _mbglMap->setZoom(newZoom, center, MGLDurationFromTimeInterval(MGLAnimationDuration)); @@ -1723,7 +1726,7 @@ public: if (twoFingerTap.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureTwoFingerSingleTap forRecognizer:twoFingerTap]; + [self trackGestureEvent:MMEEventGestureTwoFingerSingleTap forRecognizer:twoFingerTap]; [self notifyGestureDidBegin]; } @@ -1762,7 +1765,7 @@ public: if (quickZoom.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureQuickZoom forRecognizer:quickZoom]; + [self trackGestureEvent:MMEEventGestureQuickZoom forRecognizer:quickZoom]; self.scale = powf(2, _mbglMap->getZoom()); @@ -1807,7 +1810,7 @@ public: if (twoFingerDrag.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePitchStart forRecognizer:twoFingerDrag]; + [self trackGestureEvent:MMEEventGesturePitchStart forRecognizer:twoFingerDrag]; [self notifyGestureDidBegin]; } @@ -2012,11 +2015,11 @@ public: CLLocationCoordinate2D gestureCoordinate = [self convertPoint:pointInView toCoordinateFromView:recognizer.view]; int zoom = round([self zoomLevel]); - [MGLMapboxEvents pushEvent:MGLEventTypeMapTap withAttributes:@{ - MGLEventKeyLatitude: @(gestureCoordinate.latitude), - MGLEventKeyLongitude: @(gestureCoordinate.longitude), - MGLEventKeyZoomLevel: @(zoom), - MGLEventKeyGestureID: gestureID + [MGLMapboxEvents pushEvent:MMEEventTypeMapTap withAttributes:@{ + MMEEventKeyLatitude: @(gestureCoordinate.latitude), + MMEEventKeyLongitude: @(gestureCoordinate.longitude), + MMEEventKeyZoomLevel: @(zoom), + MMEEventKeyGestureID: gestureID }]; } diff --git a/platform/ios/src/MGLMapboxEvents.h b/platform/ios/src/MGLMapboxEvents.h index 98f59ffd3f..cbac578798 100644 --- a/platform/ios/src/MGLMapboxEvents.h +++ b/platform/ios/src/MGLMapboxEvents.h @@ -1,44 +1,17 @@ #import <Foundation/Foundation.h> - -#import "MGLTypes.h" +#import "MMEEventsManager.h" NS_ASSUME_NONNULL_BEGIN -// Event types -extern NSString *const MGLEventTypeAppUserTurnstile; -extern NSString *const MGLEventTypeMapLoad; -extern NSString *const MGLEventTypeMapTap; -extern NSString *const MGLEventTypeMapDragEnd; -extern NSString *const MGLEventTypeLocation; - -// Event keys -extern NSString *const MGLEventKeyLatitude; -extern NSString *const MGLEventKeyLongitude; -extern NSString *const MGLEventKeyZoomLevel; -extern NSString *const MGLEventKeyGestureID; - -// Gestures -extern NSString *const MGLEventGestureSingleTap; -extern NSString *const MGLEventGestureDoubleTap; -extern NSString *const MGLEventGestureTwoFingerSingleTap; -extern NSString *const MGLEventGestureQuickZoom; -extern NSString *const MGLEventGesturePanStart; -extern NSString *const MGLEventGesturePinchStart; -extern NSString *const MGLEventGestureRotateStart; -extern NSString *const MGLEventGesturePitchStart; - -typedef NS_DICTIONARY_OF(NSString *, id) MGLMapboxEventAttributes; -typedef NS_MUTABLE_DICTIONARY_OF(NSString *, id) MGLMutableMapboxEventAttributes; - @interface MGLMapboxEvents : NSObject -+ (nullable instancetype)sharedManager; ++ (nullable instancetype)sharedInstance; -// You must call these methods from the main thread. -// -+ (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary; -+ (void)ensureMetricsOptoutExists; ++ (void)setupWithAccessToken:(NSString *)accessToken; ++ (void)pushTurnstileEvent; ++ (void)pushEvent:(NSString *)event withAttributes:(MMEMapboxEventAttributes *)attributeDictionary; + (void)flush; ++ (void)ensureMetricsOptoutExists; @end diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m index 273af5b3bc..05f291d8a0 100644 --- a/platform/ios/src/MGLMapboxEvents.m +++ b/platform/ios/src/MGLMapboxEvents.m @@ -1,646 +1,166 @@ #import "MGLMapboxEvents.h" -#import <UIKit/UIKit.h> -#import <CoreLocation/CoreLocation.h> -#import "MGLAccountManager.h" +#import "NSBundle+MGLAdditions.h" #import "NSProcessInfo+MGLAdditions.h" -#import "NSException+MGLAdditions.h" -#import "MGLAPIClient.h" -#import "MGLLocationManager.h" -#import "MGLTelemetryConfig.h" -#include <mbgl/storage/reachability.h> -#include <sys/sysctl.h> +static NSString * const MGLAPIClientUserAgentBase = @"mapbox-maps-ios"; +static NSString * const MGLMapboxAccountType = @"MGLMapboxAccountType"; +static NSString * const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled"; +static NSString * const MGLMapboxMetricsDebugLoggingEnabled = @"MGLMapboxMetricsDebugLoggingEnabled"; +static NSString * const MGLTelemetryAccessToken = @"MGLTelemetryAccessToken"; +static NSString * const MGLTelemetryBaseURL = @"MGLTelemetryBaseURL"; -// 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"; +@interface MGLMapboxEvents () -// 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 MGLEventHorizontalAccuracy = @"horizontalAccuracy"; -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 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"; - -static NSString *const MGLMapboxAccountType = @"MGLMapboxAccountType"; -static NSString *const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled"; - -// 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; -} +@property (nonatomic) MMEEventsManager *eventsManager; +@property (nonatomic) NSURL *baseURL; +@property (nonatomic, copy) NSString *accessToken; @end +@implementation MGLMapboxEvents -@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; -@property (nonatomic) NSNumber *currentAccountTypeValue; -@property (nonatomic) BOOL currentMetricsEnabledValue; - -@end - -@implementation MGLMapboxEvents { - NSString *_instanceID; - UIBackgroundTaskIdentifier _backgroundTaskIdentifier; -} + (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, - }]; + [[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]; ++ (nullable instancetype)sharedInstance { + if (NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent) { + return nil; } - return ([[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled] && - [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType] == 0 && - !isLowPowerModeEnabled); -#endif -} - - -- (BOOL)debugLoggingEnabled { - return (self.canEnableDebugLogging && - [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]); + + static dispatch_once_t onceToken; + static MGLMapboxEvents *_sharedInstance; + dispatch_once(&onceToken, ^{ + _sharedInstance = [[self alloc] init]; + }); + return _sharedInstance; } -- (instancetype) init { +- (instancetype)init { self = [super init]; if (self) { - [MGLTelemetryConfig.sharedConfig configurationFromKey:[[NSUserDefaults standardUserDefaults] objectForKey:MGLMapboxMetricsProfile]]; + _eventsManager = [[MMEEventsManager alloc] init]; + _eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsDebugLoggingEnabled]; + _eventsManager.accountType = [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType]; + _eventsManager.metricsEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled]; - _currentAccountTypeValue = @0; - _currentMetricsEnabledValue = YES; - - _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]]; - - // 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; + // It is possible for the shared instance of this class to be created because of a call to + // +[MGLAccountManager load] early on in the app lifecycle of the host application. + // If user default values for access token and base URL are available, they are stored here + // on local properties so that they can be applied later once MMEEventsManager is fully initialized + // (once -[MMEEventsManager initializeWithAccessToken:userAgentBase:hostSDKVersion:] is called. + // Normally, the telem access token and base URL are not set this way. However, overriding these values + // with user defaults can be useful for testing with an alternative (test) backend system. + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { + self.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; } - - // 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:UIApplicationDidBecomeActiveNotification object:nil]; - - // Watch for Low Power Mode change events - if (&NSProcessInfoPowerStateDidChangeNotification != NULL) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:NSProcessInfoPowerStateDidChangeNotification object:nil]; + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { + self.baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification 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; +- (void)userDefaultsDidChange:(NSNotification *)notification { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateNonDisablingConfigurationValues]; + [self updateDisablingConfigurationValuesWithNotification:notification]; + }); +} + +- (void)updateNonDisablingConfigurationValues { + self.eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]; + + // It is possible for `MGLTelemetryAccessToken` to have been set yet `userDefaultsDidChange:` + // is called before `setupWithAccessToken:` is called. + // In that case, setting the access token here will have no effect. In practice, that's fine + // because the access token value will be resolved when `setupWithAccessToken:` is called eventually + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { + self.eventsManager.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; } - if (!_instanceID) { - _instanceID = [[NSUUID UUID] UUIDString]; - NSTimeInterval twentyFourHourTimeInterval = 24 * 3600; - self.instanceIDRotationDate = [[NSDate date] dateByAddingTimeInterval:twentyFourHourTimeInterval]; + + // It is possible for `MGLTelemetryBaseURL` to have been set yet `userDefaultsDidChange:` + // is called before setupWithAccessToken: is called. + // In that case, setting the base URL here will have no effect. In practice, that's fine + // because the base URL value will be resolved when `setupWithAccessToken:` is called eventually + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { + NSURL *baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; + self.eventsManager.baseURL = baseURL; } - return _instanceID; } -- (void)userDefaultsDidChange:(NSNotification *)notification { - +- (void)updateDisablingConfigurationValuesWithNotification:(NSNotification *)notification { // Guard against over calling pause / resume if the values this implementation actually - // cares about have not changed - + // cares about have not changed. We guard because the pause and resume method checks CoreLocation's + // authorization status and that can drag on the main thread if done too many times (e.g. if the host + // app heavily uses the user defaults API and this method is called very frequently) if ([[notification object] respondsToSelector:@selector(objectForKey:)]) { NSUserDefaults *userDefaults = [notification object]; - NSNumber *accountType = [userDefaults objectForKey:MGLMapboxAccountType]; - BOOL metricsEnabled = [[userDefaults objectForKey:MGLMapboxMetricsEnabled] boolValue]; - - if (![accountType isEqualToNumber:self.currentAccountTypeValue] || metricsEnabled != self.currentMetricsEnabledValue) { - [self pauseOrResumeMetricsCollectionIfRequired]; - self.currentAccountTypeValue = accountType; - self.currentMetricsEnabledValue = metricsEnabled; - } - } - -} - -- (void)pauseOrResumeMetricsCollectionIfRequired { - - // [CLLocationManager authorizationStatus] has been found to block in some cases so - // dispatch the call to a non-UI thread - dispatch_async(self.serialQueue, ^{ - CLAuthorizationStatus status = [CLLocationManager authorizationStatus]; + NSInteger accountType = [userDefaults integerForKey:MGLMapboxAccountType]; + BOOL metricsEnabled = [userDefaults boolForKey:MGLMapboxMetricsEnabled]; - // Checking application state must be done on the main thread for safety and - // to avoid a thread sanitizer error - dispatch_async(dispatch_get_main_queue(), ^{ - UIApplication *application = [UIApplication sharedApplication]; - UIApplicationState state = application.applicationState; - - // Prevent blue status bar when host app has `when in use` permission only and it is not in foreground - if (status == kCLAuthorizationStatusAuthorizedWhenInUse && state == UIApplicationStateBackground) { - if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) { - _backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{ - [application endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; - }]; - [self flush]; - } - [self pauseMetricsCollection]; - return; - } + if (accountType != self.eventsManager.accountType || metricsEnabled != self.eventsManager.metricsEnabled) { + self.eventsManager.accountType = accountType; + self.eventsManager.metricsEnabled = metricsEnabled; - // 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 flush]; - [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; - } - - 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; + [self.eventsManager pauseOrResumeMetricsCollectionIfRequired]; } - [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 = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyEvent] = MGLEventTypeLocation; - attributes[MGLEventKeySource] = MGLEventSource; - attributes[MGLEventKeySessionId] = self.instanceID; - attributes[MGLEventKeyOperatingSystem] = self.data.iOSVersion; - NSString *currentApplicationState = [self applicationState]; - if (![currentApplicationState isEqualToString:MGLApplicationStateUnknown]) { - attributes[MGLEventKeyApplicationState] = currentApplicationState; - } - - return [self eventForAttributes:attributes attributeDictionary:attributeDictionary]; -} - -- (MGLMapboxEventAttributes *)mapLoadEventWithAttributes:(MGLMapboxEventAttributes *)attributeDictionary { - MGLMutableMapboxEventAttributes *attributes = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyEvent] = MGLEventTypeMapLoad; - attributes[MGLEventKeyCreated] = [self.rfc3339DateFormatter stringFromDate:[NSDate date]]; - attributes[MGLEventKeyVendorID] = self.data.vendorId; - attributes[MGLEventKeyModel] = self.data.model; - attributes[MGLEventKeyOperatingSystem] = self.data.iOSVersion; - attributes[MGLEventKeyResolution] = @(self.data.scale); - attributes[MGLEventKeyAccessibilityFontScale] = @([self contentSizeScale]); - attributes[MGLEventKeyOrientation] = [self deviceOrientation]; - attributes[MGLEventKeyWifi] = @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi]); - - 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 = [NSMutableDictionary dictionary]; - attributes[MGLEventKeyCreated] = [self.rfc3339DateFormatter stringFromDate:[NSDate date]]; - attributes[MGLEventKeyOrientation] = [self deviceOrientation]; - attributes[MGLEventKeyWifi] = @([[MGLReachability reachabilityForLocalWiFi] isReachableViaWiFi]); - - 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; ++ (void)setupWithAccessToken:(NSString *)accessToken { + NSString *semanticVersion = [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"]; + NSString *shortVersion = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"]; + NSString *sdkVersion = semanticVersion ?: shortVersion; + + // It is possible that an alternative access token was already set on this instance when the class was loaded + // Use it if it exists + NSString *resolvedAccessToken = [MGLMapboxEvents sharedInstance].accessToken ?: accessToken; + + [[[self sharedInstance] eventsManager] initializeWithAccessToken:resolvedAccessToken userAgentBase:MGLAPIClientUserAgentBase hostSDKVersion:sdkVersion]; + + // It is possible that an alternative base URL was set on this instance when the class was loaded + // Use it if it exists + if ([MGLMapboxEvents sharedInstance].baseURL) { + [[MGLMapboxEvents sharedInstance] eventsManager].baseURL = [MGLMapboxEvents sharedInstance].baseURL; } - - __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}]; - } else { - [strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"post", - @"debug.eventsCount": @(events.count)}]; - } - [[UIApplication sharedApplication] endBackgroundTask:_backgroundTaskIdentifier]; - _backgroundTaskIdentifier = UIBackgroundTaskInvalid; - }]; - }); -} - -- (void)startTimer { - [self.timer invalidate]; - self.timer = [NSTimer scheduledTimerWithTimeInterval:MGLFlushInterval - target:self - selector:@selector(flush) - userInfo:nil - repeats:YES]; } -- (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; ++ (void)pushTurnstileEvent { + [[[self sharedInstance] eventsManager] sendTurnstileEvent]; } -- (NSString *)applicationState { - switch ([UIApplication sharedApplication].applicationState) { - case UIApplicationStateActive: - return MGLApplicationStateForeground; - case UIApplicationStateInactive: - return MGLApplicationStateInactive; - case UIApplicationStateBackground: - return MGLApplicationStateBackground; - default: - return MGLApplicationStateUnknown; - } ++ (void)pushEvent:(NSString *)event withAttributes:(MMEMapboxEventAttributes *)attributeDictionary { + [[[self sharedInstance] eventsManager] enqueueEventWithName:event attributes:attributeDictionary]; } -- (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)flush { + [[[self sharedInstance] eventsManager] flush]; } + (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"]]; @@ -651,7 +171,7 @@ const NSTimeInterval MGLFlushInterval = 180; } } } - + 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. " @@ -663,153 +183,4 @@ const NSTimeInterval MGLFlushInterval = 180; } } -#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; - double horizontalAccuracy = round(loc.horizontalAccuracy); - NSString *formattedDate = [self.rfc3339DateFormatter stringFromDate:loc.timestamp]; - [MGLMapboxEvents pushEvent:MGLEventTypeLocation withAttributes:@{MGLEventKeyCreated: formattedDate, - MGLEventKeyLatitude: @(lat), - MGLEventKeyLongitude: @(lng), - MGLEventKeyAltitude: @(round(loc.altitude)), - MGLEventHorizontalAccuracy: @(horizontalAccuracy)}]; - } -} - -- (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 diff --git a/platform/ios/src/Mapbox-Prefix.pch b/platform/ios/src/Mapbox-Prefix.pch new file mode 100644 index 0000000000..6754020861 --- /dev/null +++ b/platform/ios/src/Mapbox-Prefix.pch @@ -0,0 +1 @@ +#import "MMENamespacedDependencies.h" |