Files
MapLibreParticleLab/platform/ios/src/MGLMapboxEvents.m
Tobrun 2d039d3c6b Merge release-ios-v3.6.0-android-v5.1.0 to master (#9170)
* [android] Update release script to support CircleCI builds (#8950)

* update release script to trigger builds on circleci now

* update release script to trigger builds on circleci now

* [core] When a layer is added, reload its source's tiles

* [android] - keep observer when timeout occurs, make observer param nullable, fixup log messages (#8919)

* [android] - avoid crashing when deleting already deleted region (#8920)

* [android] - update LOST to 2.3.0 (#8872)

* [android] - update proguard config, allow debug mimification, update OkHttp to latest version (#8944)

* [core, android, ios, macos, qt] v10 default styles

Upgraded from v9 default styles to v10 wherever the developer expects to get the latest and greatest, as well as in a couple tests where it may be beneficial to ensure that we can handle a two-digit version number in the style URL.

Cherry-picked from ed54849e99.

* [ios, macos] Updated documented default style version

MGLStyleDefaultVersion is just for Streets now. Deleted style version documentation tests because not all styles are on the same version.

Cherry-picked from ca97dd8cf4.

* [ios, macos] Undeprecated unversioned style URL factory methods

Undeprecated the unversioned style URL factory methods in MGLStyle for consistency with the Android and Qt SDKs. Added warnings about using them with the runtime styling API.

Refactored mbgl::util::default_styles to track different versions for different styles.

Cherry-picked from 9e384b78fb.

* [core, android, ios, macos] Added Traffic Day/Night to default styles

The Styles API section of the Mapbox API Documentation site now lists Traffic Day v2 and Traffic Night v2, so this change adds those styles to all the places where styles are listed.

Also switched iosapp and macosapp to unversioned style factory methods since MGLStyleDefaultVersion is no longer applicable for all styles.

Cherry-picked from 4d6f54553d.

* [android] Release android-v5.1.0-beta.2 (#8976)

* [android] url getter on sources

* [android] fix ui test filter in makefile

* [android] - build SNAPSHOT from release branch (#8958)

* [android] - update changelog for 5.1.0-beta.2

* [android] - bump version number

* [android] - Camera change listener v2.0

* [core] allow filesource url transform reset

* [android] Update attribution wordmark (#8774)

* Update wordmark on android

* Moved attribution i icon to the right of mapbox word (in mapview preview image)

* update padding and margin

* [android] update hardcoded branch name

* revert version to 5.1.0-SNAPSHOT

* [android] - MarkerView deselect events with OnMarkerViewClickListener integration (#8996)

* [android] - publish SNAPSHOT from release 5.1.0 branch (#8995)

* [ios, macos] edited identity interpolation mode documentation (#8657)

* [ios, macos] Factored out tile URL template guide

Factored out redundant tile URL template documentation from the MGLRasterSource and MGLVectorSource documentation into a jazzy guide. This documentation used to live in one place, on a method on MGLTileSource, but that method had to be moved to MGLTileSource’s concrete subclasses. A jazzy guide is easier to link to, in any case.

* [ios] Telemetry button in modal view controllers (#9027)

Fixes #8980.

* [android] - bump tools and support lib version due to SNAPSHOT dependencies (#9046)

* [android] - bump tools and support lib version for SNAPSHOT dependencies

* revert unsupported Circle CI build tools version

* [core] Make destructor virtual to avoid object splicing during destruction

* [core] add error for non-virtual destructor deletes + add virtual dtors

* [android] -  remove marker from selected markers when removing marker from annotation manager. (#9047)

* Observe layout guides (#7716)

* [ios] observe layout guides

* [ios] update changelog

* [ios, macos] Change == to = in style function initializers

* [ios] Add annotation view initializer with annotation and reuse id (#9029)

* [ios] Remove annotation view from container view when annotation removed (#9025)

The annotation container view keeps an array of annotation views that is separate from the array of subviews that is a property of the UIView parent class. This removes an annotation view from that container view array when the associated annotation is removed. This avoids issue related to previously removed annotation views continuing to be involved in map view logic around annotation view selection due to taps.

* [android] - correct bearing conversion when animating the map with jumpTo, easeTo and animateTo. (#9050)

* [ios, macos] Updated `maximumZoomLevel` description, cherry-picked #8818 (#8842)

Cherry-picked from a3e4e67ea6.

* [ios] Updated podspecs and changelog for v3.5.3 (#8870)

Cherry-picked from 25c19902a2.

* [ios] Update podspecs and changelog for iOS v3.5.4

Cherry-picked from db7bb509e9.

* [ios, macos] Updated changelogs

* [ios, macos] changed TRUE -> 'true' (#9059) fixes #9056

* [ios, macos] Light property implementation in MGLStyle (#9043)

* [ios, macos] Add MGLLight to MGLStyle

* [ios, macos] Implement Objc bindings for Light object

* [ios, macos] Remove rawLight from MGLLight and re-implement it as value class

* [ios, macos] Fix build on macos

* [ios, macos] Add MGLLight documentation, Move MGLLightPosition to MGLLight

* [ios, macos] Add MGLLight tests.

* [ios, macos] Update changelogs

* [ios, macos] Fix misspelling

* [ios, macos] Fix MGLLightAnchor enum property names

* [ios, macos] Update documentation. Improve varialble naming.

* [ios, macos] Rename MGLLightPosition to MGLSphericalPosition

* [ios, macos] Update data types of MGLSphericalPosition

* [android] - horizontally rotated in snapshot (#9083)

* Cherry-pick arabic text to release branch v3.6.0 (#9071)

* [core] Throttle tiles to redo symbol placement at most once every 300ms.
Fixes issue #8435 and prepares for pitch-scaling changes in issue #8967.

* [core] Disable letter-spacing for Arabic labels (issue #9057)

* [ios,macos] multipolygon coordinate (#8713)

* [ios] MGLMultiPolygon's coordinate property implemented

* [ios,macos] Add polylabalel to project config

* [ios,macos] Change coordinate property for MGLPolyline,MGLPolygon,MGLMultiPolygon

* [macos] Change project configuration to support polilabel

* [ios,macos] Add MGLLocationCoordinate2DFromPoint

* [ios, macos] Update changelogs

* [ios, macos] remove unnecesary variables

* [ios, macos] Add radians conversions

* [ios, macos] Add coordinate calc to MGLPolyline

* [ios, macos] Move radian fuctions to MGLGeometry_Private.h

* [ios, macos] Fix code style

* [ios, macos] Fix code indentation

* [ios, macos] Fix radian per meters constant

* [ios, macos] Add test scenarios to testPolyline

* [ios, macos] Fix test accuracy

* [ios, macos] More robust Streets localization

Added Arabic, Portuguese, and Simplified Chinese to the list of languages with specialized fields in the Mapbox Streets source.

Rely on NSBundle to select the most appropriate locale based on the user’s preferred languages.

* [ios] Fallback to Mapbox.bundle as the framework bundle (#9074)

Fixes an issue where localizations could not be found when using static builds.

Throws exception if our bundle can't be found.

* [ios] Move image resources to an asset catalog & switch to PDFs

* [ios, macos] Updated changelogs

Reorganized the changelogs with a new packaging section. Added blurbs about #9062, #8713, #9060, and #9031.

* [ios] Update pods spec for iOS v3.6.0-beta.1

* [macos] Enable View ‣ Traffic Night

Enable the View ‣ Traffic Night menu item and check it when that style is active.

* [android] - stop location updates when toggle MyLocationView state (#9099)

* [android] - LatLngBounds bearing default value (#9102)

* [ios, macos] Fix MGLLight.achor to accept style functions

* [ios] Fix annotation initializers for subclasses of MGLAnnotationView (#9104)

Use a common init function in both of the provided initializers so that subclasses of `MGLAnnotationView` written in Swift don't need to override `init(annotation, reuseIdentifier)`

* [ios] Moved `MGLLight` in jazzy table of contents (#9111)

* [android] - logo placement for creating a Mapview programatically (#9094)

* [android] - Correct logo placement for creating a Mapview programatically, fixup non default placements

* Fixed NIGHTY_TWO_DP typo to NINETY_TWO_DP

* [android] - add binding support for Light (#9013)

* [android] Update Lost to final version 3.0.0 (#9112)

* update lost to final version 3.0.0

* bump MAS version to 2.1.1

* [android] - convert dp to pixels when getting meters per pixel at. (#9048)

* Release Android v5.1.0-beta.3 (#9115)

* [android] - Changelog update for Android release v5.1.0-beta.3

* update CI & version

* reset release properties

* [android] - harden orientation changes (#9128)

* [ios] Remove filter of single metric event

* [ios] remove layout guide observers

* Migrate to GL JS–powered feedback form (#9078)

* [ios, macos] Updated feedback URL

* [ios, macos] Add referrer, heading, pitch to feedback URL

* [ios, macos] Updated changelogs for feedback changes

* [ios] Vary referrer by platform

* [android] - javadoc update for 5.1.0 release (#9138)

* [android] - javadoc update for 5.1.0 release

* oxford comma

* Grammar/spelling tweaks

* grammar tweak

* grammar tweak

* Grammar tweaks

* Grammar tweaks

* [android] fix missing access token variable issue (#9151)

* [ios] Update pods spec for iOS v3.6.0-beta.2

* [android] Cherry picking 9133 (#9145)

* [android] Fix tracking mode + camera race condition (#9133)

* [android] fix tracking mode + camera race condition

* fix resetTrackingModesIfRequired bug (comparing current camera position with target camera position

* cherry pick #9133 and update CHANGELOG

* add null check to prevent null pointer exception

* add null check in custom location engine activity to prevent null pointer exception (#9159)

* [core] Make TransformState LatLngBounds optional

* [ios] Make annotation view rotation alignment configurable (#9147)

This commit adds `rotatesWithMap` property on `MGLAnnotationView`. This
property, when set to `YES` fixes the annotation to a map such that view
follows map's rotation angle. This is useful when user wants to display
rotation-dependent annotations (e.g. sector lights).

* [android] - validate if gestures should execute (#9173)
2017-06-02 23:41:14 +02:00

782 lines
32 KiB
Objective-C

#import "MGLMapboxEvents.h"
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
#import "MGLAccountManager.h"
#import "NSProcessInfo+MGLAdditions.h"
#import "NSException+MGLAdditions.h"
#import "MGLAPIClient.h"
#import "MGLLocationManager.h"
#include <mbgl/storage/reachability.h>
#include <sys/sysctl.h>
// Event types
NSString *const MGLEventTypeAppUserTurnstile = @"appUserTurnstile";
NSString *const MGLEventTypeMapLoad = @"map.load";
NSString *const MGLEventTypeMapTap = @"map.click";
NSString *const MGLEventTypeMapDragEnd = @"map.dragend";
NSString *const MGLEventTypeLocation = @"location";
NSString *const MGLEventTypeLocalDebug = @"debug";
// Gestures
NSString *const MGLEventGestureSingleTap = @"SingleTap";
NSString *const MGLEventGestureDoubleTap = @"DoubleTap";
NSString *const MGLEventGestureTwoFingerSingleTap = @"TwoFingerTap";
NSString *const MGLEventGestureQuickZoom = @"QuickZoom";
NSString *const MGLEventGesturePanStart = @"Pan";
NSString *const MGLEventGesturePinchStart = @"Pinch";
NSString *const MGLEventGestureRotateStart = @"Rotation";
NSString *const MGLEventGesturePitchStart = @"Pitch";
// Event keys
NSString *const MGLEventKeyLatitude = @"lat";
NSString *const MGLEventKeyLongitude = @"lng";
NSString *const MGLEventKeyZoomLevel = @"zoom";
NSString *const MGLEventKeySpeed = @"speed";
NSString *const MGLEventKeyCourse = @"course";
NSString *const MGLEventKeyGestureID = @"gesture";
NSString *const 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";
// SDK event source
static NSString *const MGLEventSource = @"mapbox";
// Event application state
static NSString *const MGLApplicationStateForeground = @"Foreground";
static NSString *const MGLApplicationStateBackground = @"Background";
static NSString *const MGLApplicationStateInactive = @"Inactive";
static NSString *const MGLApplicationStateUnknown = @"Unknown";
const NSUInteger MGLMaximumEventsPerFlush = 180;
const NSTimeInterval MGLFlushInterval = 180;
@interface MGLMapboxEventsData : NSObject
@property (nonatomic) NSString *vendorId;
@property (nonatomic) NSString *model;
@property (nonatomic) NSString *iOSVersion;
@property (nonatomic) CGFloat scale;
@end
@implementation MGLMapboxEventsData
- (instancetype)init {
if (self = [super init]) {
_vendorId = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
_model = [self sysInfoByName:"hw.machine"];
_iOSVersion = [NSString stringWithFormat:@"%@ %@", [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion];
if ([UIScreen instancesRespondToSelector:@selector(nativeScale)]) {
_scale = [UIScreen mainScreen].nativeScale;
} else {
_scale = [UIScreen mainScreen].scale;
}
}
return self;
}
- (NSString *)sysInfoByName:(char *)typeSpecifier {
size_t size;
sysctlbyname(typeSpecifier, NULL, &size, NULL, 0);
char *answer = malloc(size);
sysctlbyname(typeSpecifier, answer, &size, NULL, 0);
NSString *results = [NSString stringWithCString:answer encoding: NSUTF8StringEncoding];
free(answer);
return results;
}
@end
@interface MGLMapboxEvents () <MGLLocationManagerDelegate>
@property (nonatomic) MGLMapboxEventsData *data;
@property (nonatomic, copy) NSString *appBundleId;
@property (nonatomic, readonly) NSString *instanceID;
@property (nonatomic, copy) NSString *dateForDebugLogFile;
@property (nonatomic) NSDateFormatter *rfc3339DateFormatter;
@property (nonatomic) MGLAPIClient *apiClient;
@property (nonatomic) BOOL usesTestServer;
@property (nonatomic) BOOL canEnableDebugLogging;
@property (nonatomic, getter=isPaused) BOOL paused;
@property (nonatomic) NS_MUTABLE_ARRAY_OF(MGLMapboxEventAttributes *) *eventQueue;
@property (nonatomic) dispatch_queue_t serialQueue;
@property (nonatomic) dispatch_queue_t debugLogSerialQueue;
@property (nonatomic) MGLLocationManager *locationManager;
@property (nonatomic) NSTimer *timer;
@property (nonatomic) NSDate *instanceIDRotationDate;
@property (nonatomic) NSDate *nextTurnstileSendDate;
@end
@implementation MGLMapboxEvents {
NSString *_instanceID;
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,
}];
}
}
+ (BOOL)isEnabled {
#if TARGET_OS_SIMULATOR
return NO;
#else
BOOL isLowPowerModeEnabled = NO;
if ([NSProcessInfo instancesRespondToSelector:@selector(isLowPowerModeEnabled)]) {
isLowPowerModeEnabled = [[NSProcessInfo processInfo] isLowPowerModeEnabled];
}
return ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsEnabled"] &&
[[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0 &&
!isLowPowerModeEnabled);
#endif
}
- (BOOL)debugLoggingEnabled {
return (self.canEnableDebugLogging &&
[[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]);
}
- (instancetype) init {
self = [super init];
if (self) {
_appBundleId = [[NSBundle mainBundle] bundleIdentifier];
_apiClient = [[MGLAPIClient alloc] init];
NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
_serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
_locationManager = [[MGLLocationManager alloc] init];
_locationManager.delegate = self;
_paused = YES;
[self resumeMetricsCollection];
// Events Control
_eventQueue = [[NSMutableArray alloc] init];
// Setup Date Format
_rfc3339DateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[_rfc3339DateFormatter setLocale:enUSPOSIXLocale];
[_rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"];
// Clear Any System TimeZone Cache
[NSTimeZone resetSystemTimeZone];
[_rfc3339DateFormatter setTimeZone:[NSTimeZone systemTimeZone]];
// Configure logging
if ([self isProbablyAppStoreBuild]) {
self.canEnableDebugLogging = NO;
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]) {
NSLog(@"Telemetry logging is only enabled in non-app store builds.");
}
} else {
self.canEnableDebugLogging = YES;
}
// Watch for changes to telemetry settings by the user
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidBecomeActiveNotification object:nil];
// Watch for Low Power Mode change events
if (&NSProcessInfoPowerStateDidChangeNotification != NULL) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:NSProcessInfoPowerStateDidChangeNotification object:nil];
}
}
return self;
}
// Called implicitly from any public class convenience methods.
// May return nil if this feature is disabled.
//
+ (nullable instancetype)sharedManager {
if (NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent) {
return nil;
}
static dispatch_once_t onceToken;
static MGLMapboxEvents *_sharedManager;
dispatch_once(&onceToken, ^{
_sharedManager = [[self alloc] init];
});
return _sharedManager;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self pauseMetricsCollection];
}
- (NSString *)instanceID {
if (self.instanceIDRotationDate && [[NSDate date] timeIntervalSinceDate:self.instanceIDRotationDate] >= 0) {
_instanceID = nil;
}
if (!_instanceID) {
_instanceID = [[NSUUID UUID] UUIDString];
NSTimeInterval twentyFourHourTimeInterval = 24 * 3600;
self.instanceIDRotationDate = [[NSDate date] dateByAddingTimeInterval:twentyFourHourTimeInterval];
}
return _instanceID;
}
- (void)userDefaultsDidChange:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
[self pauseOrResumeMetricsCollectionIfRequired];
});
}
- (void)pauseOrResumeMetricsCollectionIfRequired {
UIApplication *application = [UIApplication sharedApplication];
// Prevent blue status bar when host app has `when in use` permission only and it is not in foreground
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse &&
application.applicationState == UIApplicationStateBackground) {
if (_backgroundTaskIdentifier == UIBackgroundTaskInvalid) {
_backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:_backgroundTaskIdentifier];
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}];
[self flush];
}
[self pauseMetricsCollection];
return;
}
// Toggle pause based on current pause state, user opt-out state, and low-power state.
BOOL enabled = [[self class] isEnabled];
if (self.paused && enabled) {
[self resumeMetricsCollection];
} else if (!self.paused && !enabled) {
[self 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;
}
[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;
}
__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;
}
- (NSString *)applicationState {
switch ([UIApplication sharedApplication].applicationState) {
case UIApplicationStateActive:
return MGLApplicationStateForeground;
case UIApplicationStateInactive:
return MGLApplicationStateInactive;
case UIApplicationStateBackground:
return MGLApplicationStateBackground;
default:
return MGLApplicationStateUnknown;
}
}
- (NSInteger)contentSizeScale {
NSInteger result = -9999;
NSString *sc = [UIApplication sharedApplication].preferredContentSizeCategory;
if ([sc isEqualToString:UIContentSizeCategoryExtraSmall]) {
result = -3;
} else if ([sc isEqualToString:UIContentSizeCategorySmall]) {
result = -2;
} else if ([sc isEqualToString:UIContentSizeCategoryMedium]) {
result = -1;
} else if ([sc isEqualToString:UIContentSizeCategoryLarge]) {
result = 0;
} else if ([sc isEqualToString:UIContentSizeCategoryExtraLarge]) {
result = 1;
} else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraLarge]) {
result = 2;
} else if ([sc isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) {
result = 3;
} else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityMedium]) {
result = -11;
} else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityLarge]) {
result = 10;
} else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) {
result = 11;
} else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) {
result = 12;
} else if ([sc isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) {
result = 13;
}
return result;
}
+ (void)ensureMetricsOptoutExists {
NSNumber *shownInAppNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxMetricsEnabledSettingShownInApp"];
BOOL metricsEnabledSettingShownInAppFlag = [shownInAppNumber boolValue];
if (!metricsEnabledSettingShownInAppFlag &&
[[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0) {
// Opt-out is not configured in UI, so check for Settings.bundle
id defaultEnabledValue;
NSString *appSettingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
if (appSettingsBundle) {
// Dynamic Settings.bundle loading based on http://stackoverflow.com/a/510329/2094275
NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[appSettingsBundle stringByAppendingPathComponent:@"Root.plist"]];
NSArray *preferences = settings[@"PreferenceSpecifiers"];
for (NSDictionary *prefSpecification in preferences) {
if ([prefSpecification[@"Key"] isEqualToString:@"MGLMapboxMetricsEnabled"]) {
defaultEnabledValue = prefSpecification[@"DefaultValue"];
}
}
}
if (!defaultEnabledValue) {
[NSException raise:@"Telemetry opt-out missing" format:
@"End users must be able to opt out of Mapbox Telemetry in your app, either inside Settings (via Settings.bundle) or inside this app. "
@"By default, this opt-out control is included as a menu item in the attribution action sheet. "
@"If you reimplement the opt-out control inside this app, disable this assertion by setting MGLMapboxMetricsEnabledSettingShownInApp to YES in Info.plist."
@"\n\nSee https://www.mapbox.com/ios-sdk/#telemetry_opt_out for more information."
@"\n\nAdditionally, by hiding this attribution control you agree to display the required attribution elsewhere in this app."];
}
}
}
#pragma mark CLLocationManagerUtilityDelegate
- (void)locationManager:(MGLLocationManager *)locationManager didUpdateLocations:(NSArray *)locations {
for (CLLocation *loc in locations) {
double accuracy = 10000000;
double lat = floor(loc.coordinate.latitude * accuracy) / accuracy;
double lng = floor(loc.coordinate.longitude * accuracy) / accuracy;
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