Commit 3f76b629 authored by Chris Scott's avatar Chris Scott

Experimental extraction of core-features to separate library

parent 35eba74a
#import <CoreLocation/CoreLocation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <Cordova/CDVCommandDelegate.h>
@interface BackgroundGeolocation : NSObject <CLLocationManagerDelegate>
@property (nonatomic, strong) NSString* syncCallbackId;
@property (nonatomic, strong) NSMutableArray* stationaryRegionListeners;
@property (nonatomic, weak) id <CDVCommandDelegate> commandDelegate;
- (void) configure:(CDVInvokedUrlCommand*)config;
- (void) start:(CDVInvokedUrlCommand*)command;
- (void) stop:(CDVInvokedUrlCommand*)command;
- (void) finish:(CDVInvokedUrlCommand*)command;
- (void) onPaceChange:(CDVInvokedUrlCommand*)command;
- (void) setConfig:(CDVInvokedUrlCommand*)command;
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command;
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command;
- (void) onSuspend:(NSNotification *)notification;
- (void) onResume:(NSNotification *)notification;
- (void) onAppTerminate;
@end
//
// BackgroundGeolocation.m
// Cordova Background GeoLocation
//
// Created by Christopher Scott on 2015-04-23.
//
//
#import "BackgroundGeolocation.h"
#import <Cordova/CDVPlugin.h>
#import <Cordova/CDVJSON.h>
// Debug sounds for bg-geolocation life-cycle events.
// http://iphonedevwiki.net/index.php/AudioServices
#define exitRegionSound 1005
#define locationSyncSound 1004
#define paceChangeYesSound 1110
#define paceChangeNoSound 1112
#define acquiringLocationSound 1103
#define acquiredLocationSound 1052
#define locationErrorSound 1073
@implementation BackgroundGeolocation {
BOOL isDebugging;
BOOL enabled;
BOOL isUpdatingLocation;
BOOL stopOnTerminate;
NSString *token;
NSString *url;
UIBackgroundTaskIdentifier bgTask;
NSDate *lastBgTaskAt;
NSError *locationError;
BOOL isMoving;
NSNumber *maxBackgroundHours;
CLLocationManager *locationManager;
UILocalNotification *localNotification;
NSDictionary *locationData;
CLLocation *lastLocation;
NSMutableArray *locationQueue;
NSDate *suspendedAt;
CLLocation *stationaryLocation;
CLCircularRegion *stationaryRegion;
NSInteger locationAcquisitionAttempts;
BOOL isAcquiringStationaryLocation;
NSInteger maxStationaryLocationAttempts;
BOOL isAcquiringSpeed;
NSInteger maxSpeedAcquistionAttempts;
// @config params
NSInteger stationaryRadius;
NSInteger distanceFilter;
CLLocationAccuracy desiredAccuracy;
CLActivityType activityType;
BOOL disableElasticity;
}
@synthesize syncCallbackId, stationaryRegionListeners, commandDelegate;
- (id) init
{
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
localNotification = [[UILocalNotification alloc] init];
localNotification.timeZone = [NSTimeZone defaultTimeZone];
locationQueue = [[NSMutableArray alloc] init];
// @config params
isDebugging = NO;
stopOnTerminate = NO;
stationaryRadius = 50;
distanceFilter = 50;
desiredAccuracy = kCLLocationAccuracyBest;
disableElasticity = NO;
// Flags
isMoving = NO;
isUpdatingLocation = NO;
stationaryLocation = nil;
stationaryRegion = nil;
maxStationaryLocationAttempts = 4;
maxSpeedAcquistionAttempts = 3;
// Listen to suspend/resume events
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSuspend:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume:) name:UIApplicationWillEnterForegroundNotification object:nil];
bgTask = UIBackgroundTaskInvalid;
return [super init];
}
- (void) configure:(CDVInvokedUrlCommand*)command
{
NSDictionary *config = [command.arguments objectAtIndex:0];
[self applyConfig:config];
self.syncCallbackId = command.callbackId;
locationManager.activityType = activityType;
locationManager.pausesLocationUpdatesAutomatically = YES;
locationManager.distanceFilter = distanceFilter; // meters
locationManager.desiredAccuracy = desiredAccuracy;
[self log: @"CDVBackgroundGeoLocation configure %@", config];
// ios 8 requires permissions to send local-notifications
if (isDebugging) {
UIApplication *app = [UIApplication sharedApplication];
if ([app respondsToSelector:@selector(registerUserNotificationSettings:)]) {
[app registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
}
}
}
- (void) applyConfig:(NSDictionary*)config
{
if (config[@"debug"]) {
isDebugging = [[config objectForKey:@"debug"] boolValue];
}
if (config[@"stationaryRadius"]) {
stationaryRadius = [[config objectForKey:@"stationaryRadius"] intValue];
}
if (config[@"distanceFilter"]) {
distanceFilter = [[config objectForKey:@"distanceFilter"] intValue];
}
if (config[@"desiredAccuracy"]) {
desiredAccuracy = [self decodeDesiredAccuracy:[[config objectForKey:@"desiredAccuracy"] intValue]];
}
if (config[@"activityType"]) {
activityType = [self decodeActivityType:[config objectForKey:@"activityType"]];
}
if (config[@"stopOnTerminate"]) {
stopOnTerminate = [[config objectForKey:@"stopOnTerminate"] boolValue];
}
if (config[@"disableElasticity"]) {
disableElasticity = [[config objectForKey:@"disableElasticity"] boolValue];
}
}
-(NSInteger)decodeDesiredAccuracy:(NSInteger)accuracy
{
switch (accuracy) {
case 1000:
accuracy = kCLLocationAccuracyKilometer;
break;
case 100:
accuracy = kCLLocationAccuracyHundredMeters;
break;
case 10:
accuracy = kCLLocationAccuracyNearestTenMeters;
break;
case 0:
accuracy = kCLLocationAccuracyBest;
break;
case -1:
accuracy = kCLLocationAccuracyBestForNavigation;
break;
default:
accuracy = kCLLocationAccuracyBest;
}
return accuracy;
}
-(CLActivityType)decodeActivityType:(NSString*)name
{
if ([name caseInsensitiveCompare:@"AutomotiveNavigation"]) {
return CLActivityTypeAutomotiveNavigation;
} else if ([name caseInsensitiveCompare:@"OtherNavigation"]) {
return CLActivityTypeOtherNavigation;
} else if ([name caseInsensitiveCompare:@"Fitness"]) {
return CLActivityTypeFitness;
} else {
return CLActivityTypeOther;
}
}
/**
* Turn on background geolocation
*/
- (void) start:(CDVInvokedUrlCommand*)command
{
enabled = YES;
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
[self log: @"- CDVBackgroundGeoLocation start (background? %d)", state];
[locationManager startMonitoringSignificantLocationChanges];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
}
/**
* Turn it off
*/
- (void) stop:(CDVInvokedUrlCommand*)command
{
[self log: @"- CDVBackgroundGeoLocation stop"];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
}
/**
* Change pace to moving/stopped
* @param {Boolean} isMoving
*/
- (void) onPaceChange:(CDVInvokedUrlCommand *)command
{
isMoving = [[command.arguments objectAtIndex: 0] boolValue];
[self log: @"- CDVBackgroundGeoLocation onPaceChange %d", isMoving];
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
}
- (void) setConfig:(CDVInvokedUrlCommand*)command
{
NSDictionary *config = [command.arguments objectAtIndex:0];
[self applyConfig:config];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command
{
if (self.stationaryRegionListeners == nil) {
self.stationaryRegionListeners = [[NSMutableArray alloc] init];
}
[self.stationaryRegionListeners addObject:command.callbackId];
if (stationaryRegion) {
[self queue:stationaryLocation type:@"stationary"];
}
}
/**
* Suspend. Turn on passive location services
*/
-(void) onSuspend:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation suspend (enabled? %d)", enabled];
suspendedAt = [NSDate date];
if (enabled) {
// Sample incoming stationary-location candidate: Is it within the current stationary-region? If not, I guess we're moving.
if (!isMoving && stationaryRegion) {
if ([self locationAge:stationaryLocation] < (5 * 60.0)) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Continue stationary\n%f,%f", [stationaryLocation coordinate].latitude, [stationaryLocation coordinate].longitude]];
}
[self queue:stationaryLocation type:@"stationary"];
return;
}
}
[self setPace: isMoving];
}
}
/**@
* Resume. Turn background off
*/
-(void) onResume:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation resume"];
if (enabled) {
[self stopUpdatingLocation];
}
}
/**
* toggle passive or aggressive location services
*/
- (void)setPace:(BOOL)value
{
[self log: @"- CDVBackgroundGeoLocation setPace %d, stationaryRegion? %d", value, stationaryRegion!=nil];
isMoving = value;
isAcquiringStationaryLocation = NO;
isAcquiringSpeed = NO;
locationAcquisitionAttempts = 0;
stationaryLocation = nil;
if (isDebugging) {
AudioServicesPlaySystemSound (isMoving ? paceChangeYesSound : paceChangeNoSound);
}
if (isMoving) {
if (stationaryRegion) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
isAcquiringSpeed = YES;
} else {
isAcquiringStationaryLocation = YES;
}
if (isAcquiringSpeed || isAcquiringStationaryLocation) {
// Crank up the GPS power temporarily to get a good fix on our current location
[self stopUpdatingLocation];
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
[self startUpdatingLocation];
}
}
-(void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
[self log: @"- CDVBackgroundGeoLocation didUpdateLocations (isMoving: %d)", isMoving];
locationError = nil;
if (isMoving && !isUpdatingLocation) {
[self startUpdatingLocation];
}
CLLocation *location = [locations lastObject];
if (!isMoving && !isAcquiringStationaryLocation && !stationaryLocation) {
// Perhaps our GPS signal was interupted, re-acquire a stationaryLocation now.
[self setPace: NO];
}
// test the age of the location measurement to determine if the measurement is cached
// in most cases you will not want to rely on cached measurements
if ([self locationAge:location] > 5.0) return;
// test that the horizontal accuracy does not indicate an invalid measurement
if (location.horizontalAccuracy < 0) return;
lastLocation = location;
// test the measurement to see if it is more accurate than the previous measurement
if (isAcquiringStationaryLocation) {
[self log: @"- Acquiring stationary location, accuracy: %f", location.horizontalAccuracy];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (stationaryLocation == nil || stationaryLocation.horizontalAccuracy > location.horizontalAccuracy) {
stationaryLocation = location;
}
if (++locationAcquisitionAttempts == maxStationaryLocationAttempts) {
isAcquiringStationaryLocation = NO;
[self startMonitoringStationaryRegion:stationaryLocation];
} else {
// Unacceptable stationary-location: bail-out and wait for another.
return;
}
} else if (isAcquiringSpeed) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (++locationAcquisitionAttempts == maxSpeedAcquistionAttempts) {
if (isDebugging) {
[self notify:@"Aggressive monitoring engaged"];
}
// We should have a good sample for speed now, power down our GPS as configured by user.
isAcquiringSpeed = NO;
[locationManager setDesiredAccuracy:desiredAccuracy];
[locationManager setDistanceFilter:[self calculateDistanceFilter:location.speed]];
[self startUpdatingLocation];
} else {
return;
}
} else if (isMoving) {
// Adjust distanceFilter incrementally based upon current speed
float newDistanceFilter = [self calculateDistanceFilter:location.speed];
if (newDistanceFilter != locationManager.distanceFilter) {
[self log: @"- CDVBackgroundGeoLocation updated distanceFilter, new: %f, old: %f", newDistanceFilter, locationManager.distanceFilter];
[locationManager setDistanceFilter:newDistanceFilter];
[self startUpdatingLocation];
}
} else if ([self locationIsBeyondStationaryRegion:location]) {
if (isDebugging) {
[self notify:@"Manual stationary exit-detection"];
}
[self setPace:YES];
}
[self queue:location type:@"current"];
}
/**
* Creates a new circle around user and region-monitors it for exit
*/
- (void) startMonitoringStationaryRegion:(CLLocation*)location {
stationaryLocation = location;
// fire onStationary @event for Javascript.
[self queue:location type:@"stationary"];
CLLocationCoordinate2D coord = [location coordinate];
[self log: @"- CDVBackgroundGeoLocation createStationaryRegion (%f,%f)", coord.latitude, coord.longitude];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Acquired stationary location\n%f, %f", location.coordinate.latitude,location.coordinate.longitude]];
}
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
}
isAcquiringStationaryLocation = NO;
stationaryRegion = [[CLCircularRegion alloc] initWithCenter: coord radius:stationaryRadius identifier:@"BackgroundGeoLocation stationary region"];
stationaryRegion.notifyOnExit = YES;
[locationManager startMonitoringForRegion:stationaryRegion];
[self stopUpdatingLocation];
locationManager.distanceFilter = distanceFilter;
locationManager.desiredAccuracy = desiredAccuracy;
}
/**
* Manual stationary location his-testing. This seems to help stationary-exit detection in some places where the automatic geo-fencing soesn't
*/
-(bool)locationIsBeyondStationaryRegion:(CLLocation*)location
{
[self log: @"- CDVBackgroundGeoLocation locationIsBeyondStationaryRegion"];
if (![stationaryRegion containsCoordinate:[location coordinate]]) {
double pointDistance = [stationaryLocation distanceFromLocation:location];
return (pointDistance - stationaryLocation.horizontalAccuracy - location.horizontalAccuracy) > stationaryRadius;
} else {
return NO;
}
}
/**
* Calculates distanceFilter by rounding speed to nearest 5 and multiplying by 10.
* - Clamped at 1km max.
* - Disabled by #disableElasticity
*/
-(float) calculateDistanceFilter:(float)speed
{
if (disableElasticity == YES) {
return distanceFilter;
}
float newDistanceFilter = distanceFilter;
if (speed < 100) {
// (rounded-speed-to-nearest-5) / 2)^2
// eg 5.2 becomes (5/2)^2
newDistanceFilter = pow((5.0 * floorf(fabsf(speed) / 5.0 + 0.5f)), 2) + distanceFilter;
}
return (newDistanceFilter < 1000) ? newDistanceFilter : 1000;
}
-(void) queue:(CLLocation*)location type:(id)type
{
[self log: @"- CDVBackgroundGeoLocation queue %@", type];
NSMutableDictionary *data = [self locationToHash:location];
[data setObject:type forKey:@"location_type"];
[locationQueue addObject:data];
[self flushQueue];
}
- (void) flushQueue
{
// Sanity-check the duration of last bgTask: If greater than 30s, kill it.
if (bgTask != UIBackgroundTaskInvalid) {
if (-[lastBgTaskAt timeIntervalSinceNow] > 30.0) {
[self log: @"- CDVBackgroundGeoLocation#flushQueue has to kill an out-standing background-task!"];
[self stopBackgroundTask];
}
return;
}
if ([locationQueue count] > 0) {
NSMutableDictionary *data = [locationQueue lastObject];
[locationQueue removeObject:data];
// Create a background-task and delegate to Javascript for syncing location
bgTask = [self createBackgroundTask];
[self.commandDelegate runInBackground:^{
[self sync:data];
}];
}
}
/**
* We are running in the background if this is being executed.
* We can't assume normal network access.
* bgTask is defined as an instance variable of type UIBackgroundTaskIdentifier
*/
-(void) sync:(NSMutableDictionary*)data
{
[self log: @"- CDVBackgroundGeoLocation#sync"];
[self log: @" type: %@, position: %@,%@ speed: %@", [data objectForKey:@"location_type"], [data objectForKey:@"latitude"], [data objectForKey:@"longitude"], [data objectForKey:@"speed"]];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Location update: %s\nSPD: %0.0f | DF: %ld | ACY: %0.0f",
((isMoving) ? "MOVING" : "STATIONARY"),
[[data objectForKey:@"speed"] doubleValue],
(long) locationManager.distanceFilter,
[[data objectForKey:@"accuracy"] doubleValue]]];
AudioServicesPlaySystemSound (locationSyncSound);
}
// Build a resultset for javascript callback.
NSString *locationType = [data objectForKey:@"location_type"];
if ([locationType isEqualToString:@"stationary"]) {
[self fireStationaryRegionListeners:data];
} else if ([locationType isEqualToString:@"current"]) {
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.syncCallbackId];
} else {
[self log: @"- CDVBackgroundGeoLocation#sync could not determine location_type."];
[self stopBackgroundTask];
}
}
/**
* Called by js to signify the end of a background-geolocation event
*/
-(void) finish:(CDVInvokedUrlCommand*)command
{
[self log: @"- CDVBackgroundGeoLocation finish"];
[self stopBackgroundTask];
}
- (void) fireStationaryRegionListeners:(NSMutableDictionary*)data
{
[self log: @"- CDVBackgroundGeoLocation#fireStationaryRegionListener"];
if (![self.stationaryRegionListeners count]) {
[self stopBackgroundTask];
return;
}
// Any javascript stationaryRegion event-listeners?
[data setObject:[NSNumber numberWithDouble:stationaryRadius] forKey:@"radius"];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data];
[result setKeepCallbackAsBool:YES];
for (NSString *callbackId in self.stationaryRegionListeners)
{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
}
/**
* Fetches current stationaryLocation
*/
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command
{
[self log: @"- CDVBackgroundGeoLocation getStationaryLocation"];
// Build a resultset for javascript callback.
CDVPluginResult* result = nil;
if (stationaryLocation) {
NSMutableDictionary *returnInfo = [self locationToHash:stationaryLocation];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:returnInfo];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:NO];
}
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (bool) stationaryRegionContainsLocation:(CLLocation*)location {
CLCircularRegion *region = [locationManager.monitoredRegions member:stationaryRegion];
return ([region containsCoordinate:location.coordinate]) ? YES : NO;
}
-(UIBackgroundTaskIdentifier) createBackgroundTask
{
lastBgTaskAt = [NSDate date];
return [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self stopBackgroundTask];
}];
}
- (void) stopBackgroundTask
{
UIApplication *app = [UIApplication sharedApplication];
[self log: @"- CDVBackgroundGeoLocation stopBackgroundTask (remaining t: %f)", app.backgroundTimeRemaining];
if (bgTask != UIBackgroundTaskInvalid)
{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
[self flushQueue];
}
-(NSMutableDictionary*) locationToHash:(CLLocation*)location
{
NSMutableDictionary *returnInfo;
returnInfo = [NSMutableDictionary dictionaryWithCapacity:10];
NSNumber* timestamp = [NSNumber numberWithDouble:([location.timestamp timeIntervalSince1970] * 1000)];
[returnInfo setObject:timestamp forKey:@"timestamp"];
[returnInfo setObject:[NSNumber numberWithDouble:location.speed] forKey:@"speed"];
[returnInfo setObject:[NSNumber numberWithDouble:location.verticalAccuracy] forKey:@"altitudeAccuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.horizontalAccuracy] forKey:@"accuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.course] forKey:@"heading"];
[returnInfo setObject:[NSNumber numberWithDouble:location.altitude] forKey:@"altitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.latitude] forKey:@"latitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.longitude] forKey:@"longitude"];
return returnInfo;
}
- (NSTimeInterval) locationAge:(CLLocation*)location
{
return -[location.timestamp timeIntervalSinceNow];
}
- (void) stopUpdatingLocation
{
[locationManager stopUpdatingLocation];
isUpdatingLocation = NO;
}
- (void) startUpdatingLocation
{
SEL requestSelector = NSSelectorFromString(@"requestAlwaysAuthorization");
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined && [locationManager respondsToSelector:requestSelector]) {
((void (*)(id, SEL))[locationManager methodForSelector:requestSelector])(locationManager, requestSelector);
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
} else {
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
}
}
- (void) locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
[self log: @"- CDVBackgroundGeoLocation didChangeAuthorizationStatus %u", status];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Authorization status changed %u", status]];
}
}
- (void) notify:(NSString*)message
{
localNotification.fireDate = [NSDate date];
localNotification.alertBody = message;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
}
/**
* Log a message. Only outputs when @config debug: true
*/
- (void) log:(NSString *)format, ...
{
if (isDebugging) {
va_list args;
va_start(args, format);
va_end(args);
NSLogv(format, args);
}
}
/**
* Called when user exits their stationary radius (ie: they walked ~50m away from their last recorded location.
* - turn on more aggressive location monitoring.
*/
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
[self log: @"- CDVBackgroundGeoLocation exit region"];
if (isDebugging) {
AudioServicesPlaySystemSound (exitRegionSound);
[self notify:@"Exit stationary region"];
}
[self setPace:YES];
}
/**
* 1. turn off std location services
* 2. turn on significantChanges API
* 3. create a region and start monitoring exits.
*/
- (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager
{
[self log: @"- CDVBackgroundGeoLocation paused location updates"];
if (isDebugging) {
[self notify:@"Stop detected"];
}
if (locationError) {
isMoving = NO;
[self startMonitoringStationaryRegion:lastLocation];
[self stopUpdatingLocation];
} else {
[self setPace:NO];
}
}
/**
* 1. Turn off significantChanges ApI
* 2. turn on std. location services
* 3. nullify stationaryRegion
*/
- (void)locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager
{
[self log: @"- CDVBackgroundGeoLocation resume location updates"];
if (isDebugging) {
[self notify:@"Resume location updates"];
}
[self setPace:YES];
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
[self log: @"- CDVBackgroundGeoLocation locationManager failed: %@", error];
if (isDebugging) {
AudioServicesPlaySystemSound (locationErrorSound);
[self notify:[NSString stringWithFormat:@"Location error: %@", error.localizedDescription]];
}
locationError = error;
switch(error.code) {
case kCLErrorLocationUnknown:
case kCLErrorNetwork:
case kCLErrorRegionMonitoringDenied:
case kCLErrorRegionMonitoringSetupDelayed:
case kCLErrorRegionMonitoringResponseDelayed:
case kCLErrorGeocodeFoundNoResult:
case kCLErrorGeocodeFoundPartialResult:
case kCLErrorGeocodeCanceled:
break;
case kCLErrorDenied:
[self stopUpdatingLocation];
break;
default:
[self stopUpdatingLocation];
}
}
/**
* Termination. Checks to see if it should turn off
*/
-(void) onAppTerminate
{
[self log: @"- CDVBackgroundGeoLocation appTerminate"];
if (enabled && stopOnTerminate) {
[self log: @"- CDVBackgroundGeoLocation stoping on terminate"];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
}
}
- (void)dealloc
{
locationManager.delegate = nil;
}
@end
...@@ -5,13 +5,8 @@ ...@@ -5,13 +5,8 @@
// //
#import <Cordova/CDVPlugin.h> #import <Cordova/CDVPlugin.h>
#import "CDVLocation.h"
#import <AudioToolbox/AudioToolbox.h>
@interface CDVBackgroundGeoLocation : CDVPlugin <CLLocationManagerDelegate> @interface CDVBackgroundGeoLocation : CDVPlugin
@property (nonatomic, strong) NSString* syncCallbackId;
@property (nonatomic, strong) NSMutableArray* stationaryRegionListeners;
- (void) configure:(CDVInvokedUrlCommand*)command; - (void) configure:(CDVInvokedUrlCommand*)command;
- (void) start:(CDVInvokedUrlCommand*)command; - (void) start:(CDVInvokedUrlCommand*)command;
...@@ -21,9 +16,6 @@ ...@@ -21,9 +16,6 @@
- (void) setConfig:(CDVInvokedUrlCommand*)command; - (void) setConfig:(CDVInvokedUrlCommand*)command;
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command; - (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command;
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command; - (void) getStationaryLocation:(CDVInvokedUrlCommand *)command;
- (void) onSuspend:(NSNotification *)notification;
- (void) onResume:(NSNotification *)notification;
- (void) onAppTerminate;
@end @end
...@@ -3,99 +3,17 @@ ...@@ -3,99 +3,17 @@
// //
// Created by Chris Scott <chris@transistorsoft.com> on 2013-06-15 // Created by Chris Scott <chris@transistorsoft.com> on 2013-06-15
// //
#import "CDVLocation.h"
#import "CDVBackgroundGeoLocation.h" #import "CDVBackgroundGeoLocation.h"
#import <Cordova/CDVJSON.h> #import "BackgroundGeolocation.h"
// Debug sounds for bg-geolocation life-cycle events.
// http://iphonedevwiki.net/index.php/AudioServices
#define exitRegionSound 1005
#define locationSyncSound 1004
#define paceChangeYesSound 1110
#define paceChangeNoSound 1112
#define acquiringLocationSound 1103
#define acquiredLocationSound 1052
#define locationErrorSound 1073
@implementation CDVBackgroundGeoLocation { @implementation CDVBackgroundGeoLocation {
BOOL isDebugging; BackgroundGeolocation *bgGeo;
BOOL enabled;
BOOL isUpdatingLocation;
BOOL stopOnTerminate;
NSString *token;
NSString *url;
UIBackgroundTaskIdentifier bgTask;
NSDate *lastBgTaskAt;
NSError *locationError;
BOOL isMoving;
NSNumber *maxBackgroundHours;
CLLocationManager *locationManager;
UILocalNotification *localNotification;
CDVLocationData *locationData;
CLLocation *lastLocation;
NSMutableArray *locationQueue;
NSDate *suspendedAt;
CLLocation *stationaryLocation;
CLCircularRegion *stationaryRegion;
NSInteger locationAcquisitionAttempts;
BOOL isAcquiringStationaryLocation;
NSInteger maxStationaryLocationAttempts;
BOOL isAcquiringSpeed;
NSInteger maxSpeedAcquistionAttempts;
// @config params
NSInteger stationaryRadius;
NSInteger distanceFilter;
CLLocationAccuracy desiredAccuracy;
CLActivityType activityType;
BOOL disableElasticity;
} }
@synthesize syncCallbackId;
@synthesize stationaryRegionListeners;
- (void)pluginInitialize - (void)pluginInitialize
{ {
// @config params bgGeo = [[BackgroundGeolocation alloc] init];
isDebugging = NO; bgGeo.commandDelegate = self.commandDelegate;
stopOnTerminate = NO;
stationaryRadius = 50;
distanceFilter = 50;
desiredAccuracy = kCLLocationAccuracyBest;
disableElasticity = NO;
activityType = CLActivityTypeOther;
// background location cache, for when no network is detected.
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
localNotification = [[UILocalNotification alloc] init];
localNotification.timeZone = [NSTimeZone defaultTimeZone];
locationQueue = [[NSMutableArray alloc] init];
// Flags
isMoving = NO;
isUpdatingLocation = NO;
stationaryLocation = nil;
stationaryRegion = nil;
maxStationaryLocationAttempts = 4;
maxSpeedAcquistionAttempts = 3;
bgTask = UIBackgroundTaskInvalid;
// Listen to suspend/resume events
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSuspend:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume:) name:UIApplicationWillEnterForegroundNotification object:nil];
} }
/** /**
* configure plugin * configure plugin
...@@ -106,143 +24,12 @@ ...@@ -106,143 +24,12 @@
*/ */
- (void) configure:(CDVInvokedUrlCommand*)command - (void) configure:(CDVInvokedUrlCommand*)command
{ {
NSDictionary *config = [command.arguments objectAtIndex:0]; [bgGeo configure:command];
[self applyConfig:config];
self.syncCallbackId = command.callbackId;
locationManager.activityType = activityType;
locationManager.pausesLocationUpdatesAutomatically = YES;
locationManager.distanceFilter = distanceFilter; // meters
locationManager.desiredAccuracy = desiredAccuracy;
// ios 8 requires permissions to send local-notifications
if (isDebugging) {
UIApplication *app = [UIApplication sharedApplication];
if ([app respondsToSelector:@selector(registerUserNotificationSettings:)]) {
[app registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
}
}
} }
- (void) setConfig:(CDVInvokedUrlCommand*)command - (void) setConfig:(CDVInvokedUrlCommand*)command
{ {
NSDictionary *config = [command.arguments objectAtIndex:0]; [bgGeo setConfig:command];
[self applyConfig:config];
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}
- (void) applyConfig:(NSDictionary*)config
{
if (config[@"debug"]) {
isDebugging = [[config objectForKey:@"debug"] boolValue];
}
if (config[@"stationaryRadius"]) {
stationaryRadius = [[config objectForKey:@"stationaryRadius"] intValue];
}
if (config[@"distanceFilter"]) {
distanceFilter = [[config objectForKey:@"distanceFilter"] intValue];
}
if (config[@"desiredAccuracy"]) {
desiredAccuracy = [self decodeDesiredAccuracy:[[config objectForKey:@"desiredAccuracy"] intValue]];
}
if (config[@"activityType"]) {
activityType = [self decodeActivityType:[config objectForKey:@"activityType"]];
}
if (config[@"stopOnTerminate"]) {
stopOnTerminate = [[config objectForKey:@"stopOnTerminate"] boolValue];
}
if (config[@"disableElasticity"]) {
disableElasticity = [[config objectForKey:@"disableElasticity"] boolValue];
}
[self log: @"- CDVBackgroundGeoLocation config %@", config];
}
/**
* Log a message. Only outputs when @config debug: true
*/
- (void) log:(NSString *)format, ...
{
if (isDebugging) {
va_list args;
va_start(args, format);
va_end(args);
NSLogv(format, args);
}
}
- (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command
{
if (self.stationaryRegionListeners == nil) {
self.stationaryRegionListeners = [[NSMutableArray alloc] init];
}
[self.stationaryRegionListeners addObject:command.callbackId];
if (stationaryRegion) {
[self queue:stationaryLocation type:@"stationary"];
}
}
- (void) flushQueue
{
// Sanity-check the duration of last bgTask: If greater than 30s, kill it.
if (bgTask != UIBackgroundTaskInvalid) {
if (-[lastBgTaskAt timeIntervalSinceNow] > 30.0) {
[self log: @"- CDVBackgroundGeoLocation#flushQueue has to kill an out-standing background-task!"];
[self stopBackgroundTask];
}
return;
}
if ([locationQueue count] > 0) {
NSMutableDictionary *data = [locationQueue lastObject];
[locationQueue removeObject:data];
// Create a background-task and delegate to Javascript for syncing location
bgTask = [self createBackgroundTask];
[self.commandDelegate runInBackground:^{
[self sync:data];
}];
}
}
-(NSInteger)decodeDesiredAccuracy:(NSInteger)accuracy
{
switch (accuracy) {
case 1000:
accuracy = kCLLocationAccuracyKilometer;
break;
case 100:
accuracy = kCLLocationAccuracyHundredMeters;
break;
case 10:
accuracy = kCLLocationAccuracyNearestTenMeters;
break;
case 0:
accuracy = kCLLocationAccuracyBest;
break;
case -1:
accuracy = kCLLocationAccuracyBestForNavigation;
break;
default:
accuracy = kCLLocationAccuracyBest;
}
return accuracy;
}
-(CLActivityType)decodeActivityType:(NSString*)name
{
if ([name caseInsensitiveCompare:@"AutomotiveNavigation"]) {
return CLActivityTypeAutomotiveNavigation;
} else if ([name caseInsensitiveCompare:@"OtherNavigation"]) {
return CLActivityTypeOtherNavigation;
} else if ([name caseInsensitiveCompare:@"Fitness"]) {
return CLActivityTypeFitness;
} else {
return CLActivityTypeOther;
}
} }
/** /**
...@@ -250,38 +37,14 @@ ...@@ -250,38 +37,14 @@
*/ */
- (void) start:(CDVInvokedUrlCommand*)command - (void) start:(CDVInvokedUrlCommand*)command
{ {
enabled = YES; [bgGeo start:command];
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
[self log: @"- CDVBackgroundGeoLocation start (background? %d)", state];
[locationManager startMonitoringSignificantLocationChanges];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
/** /**
* Turn it off * Turn it off
*/ */
- (void) stop:(CDVInvokedUrlCommand*)command - (void) stop:(CDVInvokedUrlCommand*)command
{ {
[self log: @"- CDVBackgroundGeoLocation stop"]; [bgGeo stop:command];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
/** /**
...@@ -290,45 +53,7 @@ ...@@ -290,45 +53,7 @@
*/ */
- (void) onPaceChange:(CDVInvokedUrlCommand *)command - (void) onPaceChange:(CDVInvokedUrlCommand *)command
{ {
isMoving = [[command.arguments objectAtIndex: 0] boolValue]; [bgGeo onPaceChange:command];
[self log: @"- CDVBackgroundGeoLocation onPaceChange %d", isMoving];
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
if (state == UIApplicationStateBackground) {
[self setPace:isMoving];
}
}
/**
* toggle passive or aggressive location services
*/
- (void)setPace:(BOOL)value
{
[self log: @"- CDVBackgroundGeoLocation setPace %d, stationaryRegion? %d", value, stationaryRegion!=nil];
isMoving = value;
isAcquiringStationaryLocation = NO;
isAcquiringSpeed = NO;
locationAcquisitionAttempts = 0;
stationaryLocation = nil;
if (isDebugging) {
AudioServicesPlaySystemSound (isMoving ? paceChangeYesSound : paceChangeNoSound);
}
if (isMoving) {
if (stationaryRegion) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
isAcquiringSpeed = YES;
} else {
isAcquiringStationaryLocation = YES;
}
if (isAcquiringSpeed || isAcquiringStationaryLocation) {
// Crank up the GPS power temporarily to get a good fix on our current location
[self stopUpdatingLocation];
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
[self startUpdatingLocation];
}
} }
/** /**
...@@ -336,434 +61,20 @@ ...@@ -336,434 +61,20 @@
*/ */
- (void) getStationaryLocation:(CDVInvokedUrlCommand *)command - (void) getStationaryLocation:(CDVInvokedUrlCommand *)command
{ {
[self log: @"- CDVBackgroundGeoLocation getStationaryLocation"]; [bgGeo getStationaryLocation:command];
// Build a resultset for javascript callback.
CDVPluginResult* result = nil;
if (stationaryLocation) {
NSMutableDictionary *returnInfo = [self locationToHash:stationaryLocation];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:returnInfo];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsBool:NO];
}
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
} }
-(NSMutableDictionary*) locationToHash:(CLLocation*)location - (void) addStationaryRegionListener:(CDVInvokedUrlCommand*)command
{ {
NSMutableDictionary *returnInfo; [bgGeo addStationaryRegionListener:command];
returnInfo = [NSMutableDictionary dictionaryWithCapacity:10];
NSNumber* timestamp = [NSNumber numberWithDouble:([location.timestamp timeIntervalSince1970] * 1000)];
[returnInfo setObject:timestamp forKey:@"timestamp"];
[returnInfo setObject:[NSNumber numberWithDouble:location.speed] forKey:@"speed"];
[returnInfo setObject:[NSNumber numberWithDouble:location.verticalAccuracy] forKey:@"altitudeAccuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.horizontalAccuracy] forKey:@"accuracy"];
[returnInfo setObject:[NSNumber numberWithDouble:location.course] forKey:@"heading"];
[returnInfo setObject:[NSNumber numberWithDouble:location.altitude] forKey:@"altitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.latitude] forKey:@"latitude"];
[returnInfo setObject:[NSNumber numberWithDouble:location.coordinate.longitude] forKey:@"longitude"];
return returnInfo;
} }
/** /**
* Called by js to signify the end of a background-geolocation event * Called by js to signify the end of a background-geolocation event
*/ */
-(void) finish:(CDVInvokedUrlCommand*)command -(void) finish:(CDVInvokedUrlCommand*)command
{ {
[self log: @"- CDVBackgroundGeoLocation finish"]; [bgGeo finish:command];
[self stopBackgroundTask];
}
/**
* Suspend. Turn on passive location services
*/
-(void) onSuspend:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation suspend (enabled? %d)", enabled];
suspendedAt = [NSDate date];
if (enabled) {
// Sample incoming stationary-location candidate: Is it within the current stationary-region? If not, I guess we're moving.
if (!isMoving && stationaryRegion) {
if ([self locationAge:stationaryLocation] < (5 * 60.0)) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Continue stationary\n%f,%f", [stationaryLocation coordinate].latitude, [stationaryLocation coordinate].longitude]];
}
[self queue:stationaryLocation type:@"stationary"];
return;
}
}
[self setPace: isMoving];
}
}
/**@
* Resume. Turn background off
*/
-(void) onResume:(NSNotification *) notification
{
[self log: @"- CDVBackgroundGeoLocation resume"];
if (enabled) {
[self stopUpdatingLocation];
}
}
/**@
* Termination. Checks to see if it should turn off
*/
-(void) onAppTerminate
{
[self log: @"- CDVBackgroundGeoLocation appTerminate"];
if (enabled && stopOnTerminate) {
[self log: @"- CDVBackgroundGeoLocation stoping on terminate"];
enabled = NO;
isMoving = NO;
[self stopUpdatingLocation];
[locationManager stopMonitoringSignificantLocationChanges];
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
stationaryRegion = nil;
}
}
}
-(void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
[self log: @"- CDVBackgroundGeoLocation didUpdateLocations (isMoving: %d)", isMoving];
locationError = nil;
if (isMoving && !isUpdatingLocation) {
[self startUpdatingLocation];
}
CLLocation *location = [locations lastObject];
if (!isMoving && !isAcquiringStationaryLocation && !stationaryLocation) {
// Perhaps our GPS signal was interupted, re-acquire a stationaryLocation now.
[self setPace: NO];
}
// test the age of the location measurement to determine if the measurement is cached
// in most cases you will not want to rely on cached measurements
if ([self locationAge:location] > 5.0) return;
// test that the horizontal accuracy does not indicate an invalid measurement
if (location.horizontalAccuracy < 0) return;
lastLocation = location;
// test the measurement to see if it is more accurate than the previous measurement
if (isAcquiringStationaryLocation) {
[self log: @"- Acquiring stationary location, accuracy: %f", location.horizontalAccuracy];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (stationaryLocation == nil || stationaryLocation.horizontalAccuracy > location.horizontalAccuracy) {
stationaryLocation = location;
}
if (++locationAcquisitionAttempts == maxStationaryLocationAttempts) {
isAcquiringStationaryLocation = NO;
[self startMonitoringStationaryRegion:stationaryLocation];
} else {
// Unacceptable stationary-location: bail-out and wait for another.
return;
}
} else if (isAcquiringSpeed) {
if (isDebugging) {
AudioServicesPlaySystemSound (acquiringLocationSound);
}
if (++locationAcquisitionAttempts == maxSpeedAcquistionAttempts) {
if (isDebugging) {
[self notify:@"Aggressive monitoring engaged"];
}
// We should have a good sample for speed now, power down our GPS as configured by user.
isAcquiringSpeed = NO;
[locationManager setDesiredAccuracy:desiredAccuracy];
[locationManager setDistanceFilter:[self calculateDistanceFilter:location.speed]];
[self startUpdatingLocation];
} else {
return;
}
} else if (isMoving) {
// Adjust distanceFilter incrementally based upon current speed
float newDistanceFilter = [self calculateDistanceFilter:location.speed];
if (newDistanceFilter != locationManager.distanceFilter) {
[self log: @"- CDVBackgroundGeoLocation updated distanceFilter, new: %f, old: %f", newDistanceFilter, locationManager.distanceFilter];
[locationManager setDistanceFilter:newDistanceFilter];
[self startUpdatingLocation];
}
} else if ([self locationIsBeyondStationaryRegion:location]) {
if (isDebugging) {
[self notify:@"Manual stationary exit-detection"];
}
[self setPace:YES];
}
[self queue:location type:@"current"];
}
/**
* Manual stationary location his-testing. This seems to help stationary-exit detection in some places where the automatic geo-fencing soesn't
*/
-(bool)locationIsBeyondStationaryRegion:(CLLocation*)location
{
[self log: @"- CDVBackgroundGeoLocation locationIsBeyondStationaryRegion"];
if (![stationaryRegion containsCoordinate:[location coordinate]]) {
double pointDistance = [stationaryLocation distanceFromLocation:location];
return (pointDistance - stationaryLocation.horizontalAccuracy - location.horizontalAccuracy) > stationaryRadius;
} else {
return NO;
}
}
/**
* Calculates distanceFilter by rounding speed to nearest 5 and multiplying by 10.
* - Clamped at 1km max.
* - Disabled by #disableElasticity
*/
-(float) calculateDistanceFilter:(float)speed
{
if (disableElasticity == YES) {
return distanceFilter;
}
float newDistanceFilter = distanceFilter;
if (speed < 100) {
// (rounded-speed-to-nearest-5) / 2)^2
// eg 5.2 becomes (5/2)^2
newDistanceFilter = pow((5.0 * floorf(fabsf(speed) / 5.0 + 0.5f)), 2) + distanceFilter;
}
return (newDistanceFilter < 1000) ? newDistanceFilter : 1000;
}
-(void) queue:(CLLocation*)location type:(id)type
{
[self log: @"- CDVBackgroundGeoLocation queue %@", type];
NSMutableDictionary *data = [self locationToHash:location];
[data setObject:type forKey:@"location_type"];
[locationQueue addObject:data];
[self flushQueue];
}
-(UIBackgroundTaskIdentifier) createBackgroundTask
{
lastBgTaskAt = [NSDate date];
return [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self stopBackgroundTask];
}];
}
/**
* We are running in the background if this is being executed.
* We can't assume normal network access.
* bgTask is defined as an instance variable of type UIBackgroundTaskIdentifier
*/
-(void) sync:(NSMutableDictionary*)data
{
[self log: @"- CDVBackgroundGeoLocation#sync"];
[self log: @" type: %@, position: %@,%@ speed: %@", [data objectForKey:@"location_type"], [data objectForKey:@"latitude"], [data objectForKey:@"longitude"], [data objectForKey:@"speed"]];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Location update: %s\nSPD: %0.0f | DF: %ld | ACY: %0.0f",
((isMoving) ? "MOVING" : "STATIONARY"),
[[data objectForKey:@"speed"] doubleValue],
(long) locationManager.distanceFilter,
[[data objectForKey:@"accuracy"] doubleValue]]];
AudioServicesPlaySystemSound (locationSyncSound);
}
// Build a resultset for javascript callback.
NSString *locationType = [data objectForKey:@"location_type"];
if ([locationType isEqualToString:@"stationary"]) {
[self fireStationaryRegionListeners:data];
} else if ([locationType isEqualToString:@"current"]) {
CDVPluginResult* result = nil;
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data];
[result setKeepCallbackAsBool:YES];
[self.commandDelegate sendPluginResult:result callbackId:self.syncCallbackId];
} else {
[self log: @"- CDVBackgroundGeoLocation#sync could not determine location_type."];
[self stopBackgroundTask];
}
}
- (void) fireStationaryRegionListeners:(NSMutableDictionary*)data
{
[self log: @"- CDVBackgroundGeoLocation#fireStationaryRegionListener"];
if (![self.stationaryRegionListeners count]) {
[self stopBackgroundTask];
return;
}
// Any javascript stationaryRegion event-listeners?
[data setObject:[NSNumber numberWithDouble:stationaryRadius] forKey:@"radius"];
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:data];
[result setKeepCallbackAsBool:YES];
for (NSString *callbackId in self.stationaryRegionListeners)
{
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
}
/**
* Creates a new circle around user and region-monitors it for exit
*/
- (void) startMonitoringStationaryRegion:(CLLocation*)location {
stationaryLocation = location;
// fire onStationary @event for Javascript.
[self queue:location type:@"stationary"];
CLLocationCoordinate2D coord = [location coordinate];
[self log: @"- CDVBackgroundGeoLocation createStationaryRegion (%f,%f)", coord.latitude, coord.longitude];
if (isDebugging) {
AudioServicesPlaySystemSound (acquiredLocationSound);
[self notify:[NSString stringWithFormat:@"Acquired stationary location\n%f, %f", location.coordinate.latitude,location.coordinate.longitude]];
}
if (stationaryRegion != nil) {
[locationManager stopMonitoringForRegion:stationaryRegion];
}
isAcquiringStationaryLocation = NO;
stationaryRegion = [[CLCircularRegion alloc] initWithCenter: coord radius:stationaryRadius identifier:@"BackgroundGeoLocation stationary region"];
stationaryRegion.notifyOnExit = YES;
[locationManager startMonitoringForRegion:stationaryRegion];
[self stopUpdatingLocation];
locationManager.distanceFilter = distanceFilter;
locationManager.desiredAccuracy = desiredAccuracy;
}
- (bool) stationaryRegionContainsLocation:(CLLocation*)location {
CLCircularRegion *region = [locationManager.monitoredRegions member:stationaryRegion];
return ([region containsCoordinate:location.coordinate]) ? YES : NO;
}
- (void) stopBackgroundTask
{
UIApplication *app = [UIApplication sharedApplication];
[self log: @"- CDVBackgroundGeoLocation stopBackgroundTask (remaining t: %f)", app.backgroundTimeRemaining];
if (bgTask != UIBackgroundTaskInvalid)
{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
[self flushQueue];
}
/**
* Called when user exits their stationary radius (ie: they walked ~50m away from their last recorded location.
* - turn on more aggressive location monitoring.
*/
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
[self log: @"- CDVBackgroundGeoLocation exit region"];
if (isDebugging) {
AudioServicesPlaySystemSound (exitRegionSound);
[self notify:@"Exit stationary region"];
}
[self setPace:YES];
}
/**
* 1. turn off std location services
* 2. turn on significantChanges API
* 3. create a region and start monitoring exits.
*/
- (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager
{
[self log: @"- CDVBackgroundGeoLocation paused location updates"];
if (isDebugging) {
[self notify:@"Stop detected"];
}
if (locationError) {
isMoving = NO;
[self startMonitoringStationaryRegion:lastLocation];
[self stopUpdatingLocation];
} else {
[self setPace:NO];
}
}
/**
* 1. Turn off significantChanges ApI
* 2. turn on std. location services
* 3. nullify stationaryRegion
*/
- (void)locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager
{
[self log: @"- CDVBackgroundGeoLocation resume location updates"];
if (isDebugging) {
[self notify:@"Resume location updates"];
}
[self setPace:YES];
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
[self log: @"- CDVBackgroundGeoLocation locationManager failed: %@", error];
if (isDebugging) {
AudioServicesPlaySystemSound (locationErrorSound);
[self notify:[NSString stringWithFormat:@"Location error: %@", error.localizedDescription]];
}
locationError = error;
switch(error.code) {
case kCLErrorLocationUnknown:
case kCLErrorNetwork:
case kCLErrorRegionMonitoringDenied:
case kCLErrorRegionMonitoringSetupDelayed:
case kCLErrorRegionMonitoringResponseDelayed:
case kCLErrorGeocodeFoundNoResult:
case kCLErrorGeocodeFoundPartialResult:
case kCLErrorGeocodeCanceled:
break;
case kCLErrorDenied:
[self stopUpdatingLocation];
break;
default:
[self stopUpdatingLocation];
}
}
- (void) stopUpdatingLocation
{
[locationManager stopUpdatingLocation];
isUpdatingLocation = NO;
}
- (void) startUpdatingLocation
{
SEL requestSelector = NSSelectorFromString(@"requestAlwaysAuthorization");
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined && [locationManager respondsToSelector:requestSelector]) {
((void (*)(id, SEL))[locationManager methodForSelector:requestSelector])(locationManager, requestSelector);
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
} else {
[locationManager startUpdatingLocation];
isUpdatingLocation = YES;
}
}
- (void) locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
[self log: @"- CDVBackgroundGeoLocation didChangeAuthorizationStatus %u", status];
if (isDebugging) {
[self notify:[NSString stringWithFormat:@"Authorization status changed %u", status]];
}
}
- (NSTimeInterval) locationAge:(CLLocation*)location
{
return -[location.timestamp timeIntervalSinceNow];
}
- (void) notify:(NSString*)message
{
localNotification.fireDate = [NSDate date];
localNotification.alertBody = message;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
} }
/** /**
* If you don't stopMonitoring when application terminates, the app will be awoken still when a * If you don't stopMonitoring when application terminates, the app will be awoken still when a
...@@ -771,12 +82,6 @@ ...@@ -771,12 +82,6 @@
* Might be desirable in certain apps. * Might be desirable in certain apps.
*/ */
- (void)applicationWillTerminate:(UIApplication *)application { - (void)applicationWillTerminate:(UIApplication *)application {
}
- (void)dealloc
{
locationManager.delegate = nil;
} }
@end @end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment