Introduce MGLLocation Manager

https://github.com/mapbox/mapbox-gl-native/pull/4115

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

This also includes some changes to make the internal pause/resume API of
the MGLMapboxEvents class less complex and autonomous. The side effects
of the map view waking or sleeping are no longer required for mapbox
events to work as intended.
This commit is contained in:
Jesse Bounds
2016-02-24 18:01:48 -08:00
parent b3adb96e4a
commit 09c9100891
6 changed files with 237 additions and 130 deletions

View File

@@ -48,6 +48,8 @@
'../platform/ios/src/MGLMapboxEvents.m',
'../platform/ios/src/MGLAPIClient.h',
'../platform/ios/src/MGLAPIClient.m',
'../platform/ios/src/MGLLocationManager.h',
'../platform/ios/src/MGLLocationManager.m',
'../platform/ios/src/MGLMapView.mm',
'../platform/ios/src/MGLAccountManager_Private.h',
'../platform/ios/src/MGLAccountManager.m',

View File

@@ -0,0 +1,25 @@
#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

View File

@@ -0,0 +1,159 @@
#import "MGLLocationManager.h"
#import <UIKit/UIKit.h>
static const NSTimeInterval fiveMinuteTimeInterval = 300.0;
static const NSTimeInterval fiveSecondTimeInterval = 5.0;
static const CLLocationDistance regionRadiusLocationDistance = 300.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];
}
}
}
#pragma mark - Utilities
- (void)configurePassiveStandardLocationManager {
if (!self.standardLocationManager) {
CLLocationManager *standardLocationManager = [[CLLocationManager alloc] init];
standardLocationManager.delegate = self;
standardLocationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
standardLocationManager.distanceFilter = 1;
self.standardLocationManager = standardLocationManager;
}
}
- (void)startLocationServices {
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized ||
[CLLocationManager 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 && [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorized) {
[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:fiveMinuteTimeInterval];
self.backgroundLocationServiceTimeoutTimer = [NSTimer scheduledTimerWithTimeInterval:fiveSecondTimeInterval target:self selector:@selector(timeoutAllowedCheck) userInfo:nil repeats:YES];
}
- (void)establishRegionMonitoringForLocation:(CLLocation *)location {
CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:location.coordinate radius:regionRadiusLocationDistance identifier:MGLLocationManagerRegionIdentifier];
region.notifyOnEntry = NO;
region.notifyOnExit = YES;
[self.standardLocationManager startMonitoringForRegion:region];
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
switch (status) {
case kCLAuthorizationStatusAuthorized: // Also handles kCLAuthorizationStatusAuthorizedAlways
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 < regionRadiusLocationDistance) {
[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

View File

@@ -997,7 +997,6 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
self.dormant = NO;
[self createGLView];
[MGLMapboxEvents validate];
self.glSnapshotView.hidden = YES;

View File

@@ -39,17 +39,13 @@ extern NSString *const MGLEventGesturePitchStart;
typedef NS_DICTIONARY_OF(NSString *, id) MGLMapboxEventAttributes;
typedef NS_MUTABLE_DICTIONARY_OF(NSString *, id) MGLMutableMapboxEventAttributes;
@interface MGLMapboxEvents : NSObject <NSURLSessionDelegate>
@interface MGLMapboxEvents : NSObject
+ (nullable instancetype)sharedManager;
// You must call these methods from the main thread.
//
+ (void)pauseMetricsCollection;
+ (void)resumeMetricsCollection;
+ (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary;
+ (void)pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary;
+ (void)validate;
+ (void)ensureMetricsOptoutExists;
+ (void)flush;
+ (BOOL)checkPushEnabled;

View File

@@ -6,6 +6,7 @@
#import "NSBundle+MGLAdditions.h"
#import "NSException+MGLAdditions.h"
#import "MGLAPIClient.h"
#import "MGLLocationManager.h"
#include <mbgl/platform/darwin/reachability.h>
#include <sys/sysctl.h>
@@ -89,15 +90,12 @@ const NSTimeInterval MGLFlushInterval = 60;
@end
@interface MGLMapboxEvents () <CLLocationManagerDelegate>
@interface MGLMapboxEvents () <MGLLocationManagerDelegate>
@property (nonatomic) MGLMapboxEventsData *data;
@property (nonatomic, copy) NSString *appBundleId;
@property (nonatomic, copy) NSString *instanceID;
@property (nonatomic, copy) NSString *dateForDebugLogFile;
@property (nonatomic, copy) NSData *digicertCert;
@property (nonatomic, copy) NSData *geoTrustCert;
@property (nonatomic, copy) NSData *testServerCert;
@property (nonatomic) NSDateFormatter *rfc3339DateFormatter;
@property (nonatomic) MGLAPIClient *apiClient;
@property (nonatomic) BOOL usesTestServer;
@@ -106,7 +104,7 @@ const NSTimeInterval MGLFlushInterval = 60;
@property (nonatomic) NS_MUTABLE_ARRAY_OF(MGLMapboxEventAttributes *) *eventQueue;
@property (nonatomic) dispatch_queue_t serialQueue;
@property (nonatomic) dispatch_queue_t debugLogSerialQueue;
@property (nonatomic) CLLocationManager *locationManager;
@property (nonatomic) MGLLocationManager *locationManager;
@property (nonatomic) NSTimer *timer;
@end
@@ -147,6 +145,8 @@ const NSTimeInterval MGLFlushInterval = 60;
NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
_serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.serial", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
_locationManager = [[MGLLocationManager alloc] init];
_locationManager.delegate = self;
_paused = YES;
[self resumeMetricsCollection];
NSBundle *resourceBundle = [NSBundle mgl_frameworkBundle];
@@ -177,6 +177,7 @@ const NSTimeInterval MGLFlushInterval = 60;
} else {
self.canEnableDebugLogging = YES;
}
// Watch for changes to telemetry settings by the user
__weak MGLMapboxEvents *weakSelf = self;
@@ -186,8 +187,11 @@ const NSTimeInterval MGLFlushInterval = 60;
usingBlock:
^(NSNotification *notification) {
MGLMapboxEvents *strongSelf = weakSelf;
[strongSelf validate];
[strongSelf pauseOrResumeMetricsCollectionIfRequired];
}];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationWillEnterForegroundNotification object:nil];
}
return self;
}
@@ -201,19 +205,9 @@ const NSTimeInterval MGLFlushInterval = 60;
}
static dispatch_once_t onceToken;
static MGLMapboxEvents *_sharedManager;
void (^setupBlock)() = ^{
dispatch_once(&onceToken, ^{
_sharedManager = [[self alloc] init];
});
};
if ( ! [[NSThread currentThread] isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
setupBlock();
});
}
else {
setupBlock();
}
dispatch_once(&onceToken, ^{
_sharedManager = [[self alloc] init];
});
return _sharedManager;
}
@@ -222,50 +216,22 @@ const NSTimeInterval MGLFlushInterval = 60;
[self pauseMetricsCollection];
}
+ (void)validate {
[[MGLMapboxEvents sharedManager] validate];
}
- (void)validate {
BOOL enabledInSettings = [[self class] isEnabled];
- (void)pauseOrResumeMetricsCollectionIfRequired {
// Prevent blue status bar when host app has `when in use` permission only and it is not in foreground
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse &&
[UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
[self pauseMetricsCollection];
return;
}
// Toggle pause based on current pause state and current settings state
// Practically, a pause only occurs because of a change to an NSUserDefaultsDidChangeNotification
BOOL enabledInSettings = [[self class] isEnabled];
if (self.paused && enabledInSettings) {
[self resumeMetricsCollection];
} else if (!self.paused && !enabledInSettings) {
[self pauseMetricsCollection];
}
[self validateUpdatingLocation];
}
- (void)validateUpdatingLocation {
if (self.paused) {
[self stopUpdatingLocation];
} else {
switch ([CLLocationManager authorizationStatus]) {
case kCLAuthorizationStatusNotDetermined:
case kCLAuthorizationStatusRestricted:
case kCLAuthorizationStatusDenied:
[self stopUpdatingLocation];
break;
case kCLAuthorizationStatusAuthorized:
// Also handles kCLAuthorizationStatusAuthorizedAlways
[self startUpdatingLocation];
break;
case kCLAuthorizationStatusAuthorizedWhenInUse:
if (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) {
// Prevent blue status bar when app is not in foreground
[self stopUpdatingLocation];
} else {
[self startUpdatingLocation];
}
break;
}
}
}
+ (void)pauseMetricsCollection {
[[MGLMapboxEvents sharedManager] pauseMetricsCollection];
}
- (void)pauseMetricsCollection {
@@ -279,22 +245,7 @@ const NSTimeInterval MGLFlushInterval = 60;
[self.eventQueue removeAllObjects];
self.data = nil;
[self validateUpdatingLocation];
}
- (void)stopUpdatingLocation {
[self.locationManager stopUpdatingLocation];
// -[CLLocationManager stopMonitoringVisits] is only available in iOS 8+.
if ([self.locationManager respondsToSelector:@selector(stopMonitoringVisits)]) {
[self.locationManager stopMonitoringVisits];
}
self.locationManager = nil;
}
+ (void)resumeMetricsCollection {
[[MGLMapboxEvents sharedManager] resumeMetricsCollection];
}
- (void)resumeMetricsCollection {
@@ -305,28 +256,7 @@ const NSTimeInterval MGLFlushInterval = 60;
self.paused = NO;
self.data = [[MGLMapboxEventsData alloc] init];
[self validateUpdatingLocation];
}
- (void)startUpdatingLocation {
if (self.locationManager || self.paused) {
NSAssert(!(self.locationManager && self.paused),
@"MGLMapboxEvents should not have a CLLocationManager while paused.");
return;
}
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
locationManager.distanceFilter = 10;
[locationManager startUpdatingLocation];
// -[CLLocationManager startMonitoringVisits] is only available in iOS 8+.
if ([locationManager respondsToSelector:@selector(startMonitoringVisits)]) {
[locationManager startMonitoringVisits];
}
self.locationManager = locationManager;
[self.locationManager startUpdatingLocation];
}
+ (void)pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
@@ -403,7 +333,7 @@ const NSTimeInterval MGLFlushInterval = 60;
}
[strongSelf.apiClient postEvent:turnstileEventAttributes completionHandler:^(NSError * _Nullable error) {
if (error) {
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error",
[strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error",
@"error": error}];
return;
}
@@ -435,7 +365,7 @@ const NSTimeInterval MGLFlushInterval = 60;
self.timer = nil;
}
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"flush"}];
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"flush"}];
}
// Called implicitly from public use of +flush.
@@ -450,11 +380,11 @@ const NSTimeInterval MGLFlushInterval = 60;
__strong __typeof__(weakSelf) strongSelf = weakSelf;
[self.apiClient postEvents:events completionHandler:^(NSError * _Nullable error) {
if (error) {
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error",
[strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"Network error",
@"error": error}];
return;
}
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"post",
[strongSelf pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription: @"post",
@"debug.eventsCount": @(events.count)}];
}];
});
@@ -609,43 +539,39 @@ const NSTimeInterval MGLFlushInterval = 60;
}
}
#pragma mark CLLocationManagerDelegate
#pragma mark CLLocationManagerUtilityDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
// Iterate through locations to pass all data
- (void)locationManager:(MGLLocationManager *)locationManager didUpdateLocations:(NSArray *)locations {
for (CLLocation *loc in locations) {
[MGLMapboxEvents pushEvent:MGLEventTypeLocation withAttributes:@{
MGLEventKeyLatitude: @(loc.coordinate.latitude),
MGLEventKeyLongitude: @(loc.coordinate.longitude),
MGLEventKeySpeed: @(loc.speed),
MGLEventKeyCourse: @(loc.course),
MGLEventKeyAltitude: @(round(loc.altitude)),
MGLEventKeyHorizontalAccuracy: @(round(loc.horizontalAccuracy)),
MGLEventKeyVerticalAccuracy: @(round(loc.verticalAccuracy))
}];
[MGLMapboxEvents pushEvent:MGLEventTypeLocation
withAttributes:@{MGLEventKeyLatitude: @(loc.coordinate.latitude),
MGLEventKeyLongitude: @(loc.coordinate.longitude),
MGLEventKeySpeed: @(loc.speed),
MGLEventKeyCourse: @(loc.course),
MGLEventKeyAltitude: @(round(loc.altitude)),
MGLEventKeyHorizontalAccuracy: @(round(loc.horizontalAccuracy)),
MGLEventKeyVerticalAccuracy: @(round(loc.verticalAccuracy))}];
}
}
- (void)locationManager:(CLLocationManager *)manager didVisit:(CLVisit *)visit {
[MGLMapboxEvents pushEvent:MGLEventTypeVisit withAttributes:@{
MGLEventKeyLatitude: @(visit.coordinate.latitude),
MGLEventKeyLongitude: @(visit.coordinate.longitude),
MGLEventKeyHorizontalAccuracy: @(round(visit.horizontalAccuracy)),
MGLEventKeyArrivalDate: [[NSDate distantPast] isEqualToDate:visit.arrivalDate] ? [NSNull null] : [_rfc3339DateFormatter stringFromDate:visit.arrivalDate],
MGLEventKeyDepartureDate: [[NSDate distantFuture] isEqualToDate:visit.departureDate] ? [NSNull null] : [_rfc3339DateFormatter stringFromDate:visit.departureDate]
}];
- (void)locationManagerBackgroundLocationUpdatesDidAutomaticallyPause:(MGLLocationManager *)locationManager {
[self pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{MGLEventKeyLocalDebugDescription:@"locationManager.locationManagerAutoPause"}];
}
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
[self validateUpdatingLocation];
- (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 {
[[MGLMapboxEvents sharedManager] pushDebugEvent:event withAttributes:attributeDictionary];
}
- (void)pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
if (![self debugLoggingEnabled]) {
return;