Files
smart-city-digital-twin-mar…/smart-app-city/frontend/node_modules/expo-location/ios/EXLocation/EXLocation.m
Eric FELIXINE e30ae8ed09 feat(smart-app): implement complete mobile app MVP
- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
2026-06-01 18:00:35 -04:00

628 lines
24 KiB
Objective-C

// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXLocation.h>
#import <EXLocation/EXLocationDelegate.h>
#import <EXLocation/EXLocationTaskConsumer.h>
#import <EXLocation/EXGeofencingTaskConsumer.h>
#import <EXLocation/EXLocationPermissionRequester.h>
#import <EXLocation/EXForegroundPermissionRequester.h>
#import <EXLocation/EXBackgroundLocationPermissionRequester.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <CoreLocation/CLHeading.h>
#import <CoreLocation/CLGeocoder.h>
#import <CoreLocation/CLPlacemark.h>
#import <CoreLocation/CLError.h>
#import <CoreLocation/CLCircularRegion.h>
#import <ExpoModulesCore/EXEventEmitterService.h>
#import <ExpoModulesCore/EXPermissionsInterface.h>
#import <ExpoModulesCore/EXPermissionsMethodsDelegate.h>
#import <ExpoModulesCore/EXTaskManagerInterface.h>
NS_ASSUME_NONNULL_BEGIN
NSString * const EXLocationChangedEventName = @"Expo.locationChanged";
NSString * const EXHeadingChangedEventName = @"Expo.headingChanged";
@interface EXLocation ()
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, EXLocationDelegate*> *delegates;
@property (nonatomic, strong) NSMutableSet<EXLocationDelegate *> *retainedDelegates;
@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
@property (nonatomic, weak) id<EXPermissionsInterface> permissionsManager;
@property (nonatomic, weak) id<EXTaskManagerInterface> tasksManager;
@end
@implementation EXLocation
EX_EXPORT_MODULE(ExpoLocation);
- (instancetype)init
{
if (self = [super init]) {
_delegates = [NSMutableDictionary dictionary];
_retainedDelegates = [NSMutableSet set];
}
return self;
}
- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
{
_eventEmitter = [moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)];
_tasksManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXTaskManagerInterface)];
_permissionsManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXPermissionsInterface)];
[EXPermissionsMethodsDelegate registerRequesters:@[
[EXLocationPermissionRequester new],
[EXForegroundPermissionRequester new],
[EXBackgroundLocationPermissionRequester new]
] withPermissionsManager:_permissionsManager];
}
- (dispatch_queue_t)methodQueue
{
// Location managers must be created on the main thread
return dispatch_get_main_queue();
}
# pragma mark - EXEventEmitter
- (NSArray<NSString *> *)supportedEvents
{
return @[EXLocationChangedEventName, EXHeadingChangedEventName];
}
- (void)startObserving {}
- (void)stopObserving {}
# pragma mark - Exported methods
EX_EXPORT_METHOD_AS(getProviderStatusAsync,
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
resolve(@{
@"locationServicesEnabled": @([CLLocationManager locationServicesEnabled]),
@"backgroundModeEnabled": @([_tasksManager hasBackgroundModeEnabled:@"location"]),
});
}
EX_EXPORT_METHOD_AS(getCurrentPositionAsync,
options:(NSDictionary *)options
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
if (![self checkForegroundPermissions:reject]) {
return;
}
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
__weak typeof(self) weakSelf = self;
__block EXLocationDelegate *delegate;
delegate = [[EXLocationDelegate alloc] initWithId:nil withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> * _Nonnull locations) {
if (delegate != nil) {
if (locations.lastObject != nil) {
resolve([EXLocation exportLocation:locations.lastObject]);
} else {
reject(@"E_LOCATION_NOT_FOUND", @"Current location not found.", nil);
}
[weakSelf.retainedDelegates removeObject:delegate];
delegate = nil;
}
} onUpdateHeadings:nil onError:^(NSError *error) {
reject(@"E_LOCATION_UNAVAILABLE", [@"Cannot obtain current location: " stringByAppendingString:error.description], nil);
}];
// retain location manager delegate so it will not dealloc until onUpdateLocations gets called
[_retainedDelegates addObject:delegate];
locMgr.delegate = delegate;
[locMgr requestLocation];
}
EX_EXPORT_METHOD_AS(watchPositionImplAsync,
watchId:(nonnull NSNumber *)watchId
options:(NSDictionary *)options
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
if (![self checkForegroundPermissions:reject]) {
return;
}
__weak typeof(self) weakSelf = self;
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> *locations) {
if (locations.lastObject != nil && weakSelf != nil) {
__strong typeof(weakSelf) strongSelf = weakSelf;
CLLocation *loc = locations.lastObject;
NSDictionary *body = @{
@"watchId": watchId,
@"location": [EXLocation exportLocation:loc],
};
[strongSelf->_eventEmitter sendEventWithName:EXLocationChangedEventName body:body];
}
} onUpdateHeadings:nil onError:^(NSError *error) {
// TODO: report errors
// (ben) error could be (among other things):
// - kCLErrorDenied - we should use the same UNAUTHORIZED behavior as elsewhere
// - kCLErrorLocationUnknown - we can actually ignore this error and keep tracking
// location (I think -- my knowledge might be a few months out of date)
}];
_delegates[delegate.watchId] = delegate;
locMgr.delegate = delegate;
[locMgr startUpdatingLocation];
resolve([NSNull null]);
}
EX_EXPORT_METHOD_AS(getLastKnownPositionAsync,
getLastKnownPositionWithOptions:(NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
if (![self checkForegroundPermissions:reject]) {
return;
}
CLLocation *location = [[self locationManagerWithOptions:nil] location];
if ([self.class isLocation:location validWithOptions:options]) {
resolve([EXLocation exportLocation:location]);
} else {
resolve([NSNull null]);
}
}
// Watch method for getting compass updates
EX_EXPORT_METHOD_AS(watchDeviceHeading,
watchHeadingWithWatchId:(nonnull NSNumber *)watchId
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject) {
if (![self checkForegroundPermissions:reject]) {
return;
}
__weak typeof(self) weakSelf = self;
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
locMgr.distanceFilter = kCLDistanceFilterNone;
locMgr.desiredAccuracy = kCLLocationAccuracyBest;
locMgr.allowsBackgroundLocationUpdates = NO;
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations: nil onUpdateHeadings:^(CLHeading *newHeading) {
if (newHeading != nil && weakSelf != nil) {
__strong typeof(weakSelf) strongSelf = weakSelf;
NSNumber *accuracy;
// Convert iOS heading accuracy to Android system
// 3: high accuracy, 2: medium, 1: low, 0: none
if (newHeading.headingAccuracy > 50 || newHeading.headingAccuracy < 0) {
accuracy = @(0);
} else if (newHeading.headingAccuracy > 35) {
accuracy = @(1);
} else if (newHeading.headingAccuracy > 20) {
accuracy = @(2);
} else {
accuracy = @(3);
}
NSDictionary *body = @{@"watchId": watchId,
@"heading": @{
@"trueHeading": @(newHeading.trueHeading),
@"magHeading": @(newHeading.magneticHeading),
@"accuracy": accuracy,
},
};
[strongSelf->_eventEmitter sendEventWithName:EXHeadingChangedEventName body:body];
}
} onError:^(NSError *error) {
// Error getting updates
}];
_delegates[delegate.watchId] = delegate;
locMgr.delegate = delegate;
[locMgr startUpdatingHeading];
resolve([NSNull null]);
}
EX_EXPORT_METHOD_AS(removeWatchAsync,
watchId:(nonnull NSNumber *)watchId
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
EXLocationDelegate *delegate = _delegates[watchId];
if (delegate) {
// Unsuscribe from both location and heading updates
[delegate.locMgr stopUpdatingLocation];
[delegate.locMgr stopUpdatingHeading];
delegate.locMgr.delegate = nil;
[_delegates removeObjectForKey:watchId];
}
resolve([NSNull null]);
}
EX_EXPORT_METHOD_AS(geocodeAsync,
address:(nonnull NSString *)address
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:address completionHandler:^(NSArray* placemarks, NSError* error){
if (!error) {
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
for (CLPlacemark* placemark in placemarks) {
CLLocation *location = placemark.location;
[results addObject:@{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"accuracy": @(location.horizontalAccuracy),
}];
}
resolve(results);
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
resolve(@[]);
} else if (error.code == kCLErrorNetwork) {
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
} else {
reject(@"E_GEOCODING_FAILED", @"Error while geocoding an address", error);
}
}];
}
EX_EXPORT_METHOD_AS(reverseGeocodeAsync,
locationMap:(nonnull NSDictionary *)locationMap
resolver:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
CLLocation *location = [[CLLocation alloc] initWithLatitude:[locationMap[@"latitude"] floatValue] longitude:[locationMap[@"longitude"] floatValue]];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray* placemarks, NSError* error){
if (!error) {
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
for (CLPlacemark* placemark in placemarks) {
NSDictionary *address = @{
@"city": UMNullIfNil(placemark.locality),
@"district": UMNullIfNil(placemark.subLocality),
@"streetNumber": UMNullIfNil(placemark.subThoroughfare),
@"street": EXNullIfNil(placemark.thoroughfare),
@"region": EXNullIfNil(placemark.administrativeArea),
@"subregion": EXNullIfNil(placemark.subAdministrativeArea),
@"country": EXNullIfNil(placemark.country),
@"postalCode": EXNullIfNil(placemark.postalCode),
@"name": EXNullIfNil(placemark.name),
@"isoCountryCode": EXNullIfNil(placemark.ISOcountryCode),
@"timezone": EXNullIfNil(placemark.timeZone.name),
};
[results addObject:address];
}
resolve(results);
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
resolve(@[]);
} else if (error.code == kCLErrorNetwork) {
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
} else {
reject(@"E_REVGEOCODING_FAILED", @"Error while reverse-geocoding a location", error);
}
}];
}
EX_EXPORT_METHOD_AS(getPermissionsAsync,
getPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(requestPermissionsAsync,
requestPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(getForegroundPermissionsAsync,
getForegroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXForegroundPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(requestForegroundPermissionsAsync,
requestForegroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXForegroundPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(getBackgroundPermissionsAsync,
getBackgroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXBackgroundLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(requestBackgroundPermissionsAsync,
requestBackgroundPermissionsAsync:(EXPromiseResolveBlock)resolve
rejecter:(EXPromiseRejectBlock)reject)
{
[EXPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXBackgroundLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
EX_EXPORT_METHOD_AS(hasServicesEnabledAsync,
hasServicesEnabled:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
BOOL servicesEnabled = [CLLocationManager locationServicesEnabled];
resolve(@(servicesEnabled));
}
# pragma mark - Background location
EX_EXPORT_METHOD_AS(startLocationUpdatesAsync,
startLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
withOptions:(nonnull NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
// There are two ways of starting this service.
// 1. As a background location service, this requires the background location permission.
// 2. As a user-initiated foreground service, this does NOT require the background location permission.
// Unfortunately, we cannot distinguish between those cases.
// So we only check foreground permission which needs to be granted in both cases.
if (![self checkForegroundPermissions:reject] || ![self checkTaskManagerExists:reject] || ![self checkBackgroundServices:reject]) {
return;
}
if (![CLLocationManager significantLocationChangeMonitoringAvailable]) {
return reject(@"E_SIGNIFICANT_CHANGES_UNAVAILABLE", @"Significant location changes monitoring is not available.", nil);
}
@try {
[_tasksManager registerTaskWithName:taskName consumer:[EXLocationTaskConsumer class] options:options];
}
@catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(stopLocationUpdatesAsync,
stopLocationUpdatesForTaskWithName:(NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
@try {
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXLocationTaskConsumer class]];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(hasStartedLocationUpdatesAsync,
hasStartedLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXLocationTaskConsumer class]]));
}
# pragma mark - Geofencing
EX_EXPORT_METHOD_AS(startGeofencingAsync,
startGeofencingWithTaskName:(nonnull NSString *)taskName
withOptions:(nonnull NSDictionary *)options
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkBackgroundPermissions:reject] || ![self checkTaskManagerExists:reject]) {
return;
}
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
return reject(@"E_GEOFENCING_UNAVAILABLE", @"Geofencing is not available", nil);
}
@try {
[_tasksManager registerTaskWithName:taskName consumer:[EXGeofencingTaskConsumer class] options:options];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(stopGeofencingAsync,
stopGeofencingWithTaskName:(nonnull NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
@try {
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXGeofencingTaskConsumer class]];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
EX_EXPORT_METHOD_AS(hasStartedGeofencingAsync,
hasStartedGeofencingForTaskWithName:(NSString *)taskName
resolve:(EXPromiseResolveBlock)resolve
reject:(EXPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXGeofencingTaskConsumer class]]));
}
# pragma mark - helpers
- (CLLocationManager *)locationManagerWithOptions:(nullable NSDictionary *)options
{
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
locMgr.allowsBackgroundLocationUpdates = NO;
if (options) {
locMgr.distanceFilter = options[@"distanceInterval"] ? [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone : kCLLocationAccuracyHundredMeters;
if (options[@"accuracy"]) {
EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
locMgr.desiredAccuracy = [self.class CLLocationAccuracyFromOption:accuracy];
}
}
return locMgr;
}
- (BOOL)checkForegroundPermissions:(EXPromiseRejectBlock)reject
{
if (![CLLocationManager locationServicesEnabled]) {
reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
return NO;
}
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXForegroundPermissionRequester class]]) {
reject(@"E_NO_PERMISSIONS", @"LOCATION_FOREGROUND permission is required to do this operation.", nil);
return NO;
}
return YES;
}
- (BOOL)checkBackgroundPermissions:(EXPromiseRejectBlock)reject
{
if (![CLLocationManager locationServicesEnabled]) {
reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
return NO;
}
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXBackgroundLocationPermissionRequester class]]) {
reject(@"E_NO_PERMISSIONS", @"LOCATION_BACKGROUND permission is required to do this operation.", nil);
return NO;
}
return YES;
}
- (BOOL)checkTaskManagerExists:(EXPromiseRejectBlock)reject
{
if (_tasksManager == nil) {
reject(@"E_TASKMANAGER_NOT_FOUND", @"`expo-task-manager` module is required to use background services.", nil);
return NO;
}
return YES;
}
- (BOOL)checkBackgroundServices:(EXPromiseRejectBlock)reject
{
if (![_tasksManager hasBackgroundModeEnabled:@"location"]) {
reject(@"E_BACKGROUND_SERVICES_DISABLED", @"Background Location has not been configured. To enable it, add `location` to `UIBackgroundModes` in Info.plist file.", nil);
return NO;
}
return YES;
}
# pragma mark - static helpers
+ (NSDictionary *)exportLocation:(CLLocation *)location
{
return @{
@"coords": @{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"accuracy": @(location.horizontalAccuracy),
@"altitudeAccuracy": @(location.verticalAccuracy),
@"heading": @(location.course),
@"speed": @(location.speed),
},
@"timestamp": @([location.timestamp timeIntervalSince1970] * 1000),
};
}
+ (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy
{
switch (accuracy) {
case EXLocationAccuracyLowest:
return kCLLocationAccuracyThreeKilometers;
case EXLocationAccuracyLow:
return kCLLocationAccuracyKilometer;
case EXLocationAccuracyBalanced:
return kCLLocationAccuracyHundredMeters;
case EXLocationAccuracyHigh:
return kCLLocationAccuracyNearestTenMeters;
case EXLocationAccuracyHighest:
return kCLLocationAccuracyBest;
case EXLocationAccuracyBestForNavigation:
return kCLLocationAccuracyBestForNavigation;
default:
return kCLLocationAccuracyHundredMeters;
}
}
+ (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType
{
if (activityType >= CLActivityTypeOther && activityType <= CLActivityTypeOtherNavigation) {
return activityType;
}
if (@available(iOS 12.0, *)) {
if (activityType == CLActivityTypeAirborne) {
return activityType;
}
}
return CLActivityTypeOther;
}
+ (BOOL)isLocation:(nullable CLLocation *)location validWithOptions:(nullable NSDictionary *)options
{
if (location == nil) {
return NO;
}
NSTimeInterval maxAge = options[@"maxAge"] ? [options[@"maxAge"] doubleValue] : DBL_MAX;
CLLocationAccuracy requiredAccuracy = options[@"requiredAccuracy"] ? [options[@"requiredAccuracy"] doubleValue] : DBL_MAX;
NSTimeInterval timeDiff = -location.timestamp.timeIntervalSinceNow;
return location != nil && timeDiff * 1000 <= maxAge && location.horizontalAccuracy <= requiredAccuracy;
}
@end
NS_ASSUME_NONNULL_END