blob: 24211c4295413acbf1a2906319040f70a5cd574e [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "LocationBackgroundPlugin.h"
#import <CoreLocation/CoreLocation.h>
@implementation LocationBackgroundPlugin {
CLLocationManager *_locationManager;
FlutterEngine *_headlessEngine;
FlutterMethodChannel *_callbackChannel;
FlutterMethodChannel *_mainChannel;
NSObject<FlutterPluginRegistrar> *_registrar;
NSUserDefaults *_persistentState;
int64_t _onLocationUpdateHandle;
}
static LocationBackgroundPlugin *instance = nil;
#pragma mark FlutterPlugin Methods
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
@synchronized(self) {
if (instance == nil) {
instance = [[LocationBackgroundPlugin alloc] init:registrar];
[registrar addApplicationDelegate:instance];
}
}
}
// When iOS relaunches us due to a significant location change, we need to
// reinitialize our plugin state. This includes relaunching the headless
// service, retrieving our cached callback handles and location manager
// settings, and restarting the location manager to actually receive the
// location event.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Check to see if we're being launched due to a location event.
if (launchOptions[UIApplicationLaunchOptionsLocationKey] != nil) {
// Restart the headless service.
[self startHeadlessService:[self getCallbackDispatcherHandle]];
// Grab our callback handles and location manager state.
_onLocationUpdateHandle = [self getLocationCallbackHandle];
_locationManager.pausesLocationUpdatesAutomatically =
[self getPausesLocationUpdatesAutomatically];
if (@available(iOS 11.0, *)) {
_locationManager.showsBackgroundLocationIndicator =
[self getShowsBackgroundLocationIndicator];
}
if (@available(iOS 9.0, *)) {
_locationManager.allowsBackgroundLocationUpdates = YES;
}
// Finally, restart monitoring for location changes to get our location.
[self->_locationManager startMonitoringSignificantLocationChanges];
}
// Note: if we return NO, this vetos the launch of the application.
return YES;
}
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
NSArray *arguments = call.arguments;
if ([@"monitorLocationChanges" isEqualToString:call.method]) {
NSAssert(arguments.count == 4, @"Invalid argument count for 'monitorLocationChanges'");
[self monitorLocationChanges:arguments];
result(@(YES));
} else if ([@"startHeadlessService" isEqualToString:call.method]) {
NSAssert(arguments.count == 1, @"Invalid argument count for 'startHeadlessService'");
[self startHeadlessService:[arguments[0] longValue]];
} else if ([@"cancelLocationUpdates" isEqualToString:call.method]) {
NSAssert(arguments.count == 0, @"Invalid argument count for 'cancelLocationUpdates'");
[self stopUpdatingLocation];
result(nil);
} else {
NSLog(@"Unknown method: %@\n", call.method);
result(FlutterMethodNotImplemented);
}
}
#pragma mark LocationManagerDelegate Methods
// Location events come in here from our LocationManager and are forwarded to
// onLocationEvent.
- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray<CLLocation *> *)locations {
for (CLLocation *location in locations) {
[self onLocationEvent:location];
}
}
#pragma mark LocationBackgroundPlugin Methods
- (instancetype)init:(NSObject<FlutterPluginRegistrar> *)registrar {
self = [super init];
NSAssert(self, @"super init cannot be nil");
_persistentState = [NSUserDefaults standardUserDefaults];
_locationManager = [[CLLocationManager alloc] init];
[_locationManager setDelegate:self];
[_locationManager requestAlwaysAuthorization];
_headlessEngine = [[FlutterEngine alloc] initWithName:@"io.flutter.plugins.location_background"
project:nil];
_registrar = registrar;
// This is the method channel used to communicate with the UI Isolate.
_mainChannel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/ios_background_location"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:self channel:_mainChannel];
// This is the method channel used to communicate with
// `_backgroundCallbackDispatcher` defined in the Dart portion of our plugin.
// Note: we don't add a MethodCallDelegate for this channel now since our
// BinaryMessenger needs to be initialized first, which is done in
// `startHeadlessService` below.
_callbackChannel = [FlutterMethodChannel
methodChannelWithName:@"plugins.flutter.io/ios_background_location_callback"
binaryMessenger:_headlessEngine];
return self;
}
- (int64_t)getCallbackDispatcherHandle {
id handle = [_persistentState objectForKey:@"callback_dispatcher_handle"];
if (handle == nil) {
return 0;
}
return [handle longLongValue];
}
- (void)setCallbackDispatcherHandle:(int64_t)handle {
[_persistentState setObject:[NSNumber numberWithLongLong:handle]
forKey:@"callback_dispatcher_handle"];
}
- (int64_t)getLocationCallbackHandle {
id handle = [_persistentState objectForKey:@"location_callback_handle"];
if (handle == nil) {
return 0;
}
return [handle longLongValue];
}
- (void)setLocationCallbackHandle:(int64_t)handle {
[_persistentState setObject:[NSNumber numberWithLongLong:handle]
forKey:@"location_callback_handle"];
}
- (BOOL)getPausesLocationUpdatesAutomatically {
return [_persistentState boolForKey:@"pauses_location_updates_automatically"];
}
- (void)setPausesLocationUpdatesAutomatically:(BOOL)pause {
[_persistentState setBool:pause forKey:@"pauses_location_updates_automatically"];
}
- (BOOL)getShowsBackgroundLocationIndicator {
return [_persistentState boolForKey:@"shows_background_location_indicator"];
}
- (void)setShowsBackgroundLocationIndicator:(BOOL)pause {
[_persistentState setBool:pause forKey:@"shows_background_location_indicator"];
}
// Initializes and starts the background isolate which will process location
// events. `handle` is the handle to the callback dispatcher which we specified
// in the Dart portion of the plugin.
- (void)startHeadlessService:(int64_t)handle {
[self setCallbackDispatcherHandle:handle];
// Lookup the information for our callback dispatcher from the callback cache.
// This cache is populated when `PluginUtilities.getCallbackHandle` is called
// and the resulting handle maps to a `FlutterCallbackInformation` object.
// This object contains information needed by the engine to start a headless
// runner, which includes the callback name as well as the path to the file
// containing the callback.
FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:handle];
NSAssert(info != nil, @"failed to find callback");
NSString *entrypoint = info.callbackName;
NSString *uri = info.callbackLibraryPath;
// Here we actually launch the background isolate to start executing our
// callback dispatcher, `_backgroundCallbackDispatcher`, in Dart.
[_headlessEngine runWithEntrypoint:entrypoint libraryURI:uri];
// The headless runner needs to be initialized before we can register it as a
// MethodCallDelegate or else we get an illegal memory access. If we don't
// want to make calls from `_backgroundCallDispatcher` back to native code,
// we don't need to add a MethodCallDelegate for this channel.
[_registrar addMethodCallDelegate:self channel:_callbackChannel];
}
// Start receiving location updates.
- (void)monitorLocationChanges:(NSArray *)arguments {
_onLocationUpdateHandle = [arguments[0] longLongValue];
[self setLocationCallbackHandle:_onLocationUpdateHandle];
_locationManager.pausesLocationUpdatesAutomatically = arguments[1];
if (@available(iOS 11.0, *)) {
_locationManager.showsBackgroundLocationIndicator = arguments[2];
[self setShowsBackgroundLocationIndicator:_locationManager.showsBackgroundLocationIndicator];
}
_locationManager.activityType = [arguments[3] integerValue];
if (@available(iOS 9.0, *)) {
_locationManager.allowsBackgroundLocationUpdates = YES;
}
[self setPausesLocationUpdatesAutomatically:_locationManager.pausesLocationUpdatesAutomatically];
[self->_locationManager startMonitoringSignificantLocationChanges];
}
// Stop the location updates.
- (void)stopUpdatingLocation {
[self->_locationManager stopUpdatingLocation];
}
// Sends location events to our `_backgroundCallDispatcher` in Dart code via
// the MethodChannel we established earlier.
- (void)onLocationEvent:(CLLocation *)location {
[_callbackChannel invokeMethod:@"onLocationEvent"
arguments:@[
@(_onLocationUpdateHandle), @(location.timestamp.timeIntervalSince1970),
@(location.coordinate.latitude), @(location.coordinate.longitude),
@(location.horizontalAccuracy), @(location.speed)
]];
}
@end