// Copyright 2016-present 650 Industries. All rights reserved. #import #import #import static SEL alwaysAuthorizationSelector; @interface EXBackgroundLocationPermissionRequester () @property (nonatomic, assign) bool wasAsked; @property (nonatomic, assign) bool isWaitingForTimeout; @end @implementation EXBackgroundLocationPermissionRequester - (instancetype)init { if (self = [super init]) { _wasAsked = false; _isWaitingForTimeout = false; } return self; } + (NSString *)permissionType { return @"locationBackground"; } + (void)load { alwaysAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"AlwaysAuthorization"]); } - (void)requestLocationPermissions { if ([EXBaseLocationRequester isConfiguredForAlwaysAuthorization] && [self.locationManager respondsToSelector:alwaysAuthorizationSelector]) { _wasAsked = true; CLAuthorizationStatus status = [self.locationManager authorizationStatus]; if (status == kCLAuthorizationStatusAuthorizedWhenInUse) { // We already have a foreground permission granted: // When asking for background location, we might or might not have asked for foreground permission // before we get here. An issue here is if the user has a temporary permission ("Allow once") - which // results in the status being "kCLAuthorizationStatusAuthorizedWhenInUse" - without us knowing. // We need to handle this special case which is not possible to detect through the API. // What we do is that we'll wait 1.5 seconds on an UIApplicationWillResignActiveNotification // notification (which will be emitted almost directly if the permission dialog is displayed). If the permission // dialog is not displayed we'll timeout and can resolve the waiting promise with an updated denied status. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppBecomingInactive) name:UIApplicationWillResignActiveNotification object:nil]; // Setup timeout - if no permission dialog was displayed we can just stop listening and deny // the request [self setupAppInactivateTimeout]; } // Request permissions ((void (*)(id, SEL))objc_msgSend)(self.locationManager, alwaysAuthorizationSelector); } else { self.reject(@"ERR_LOCATION_INFO_PLIST", @"One of the `NSLocation*UsageDescription` keys must be present in Info.plist to be able to use geolocation.", nil); self.resolve = nil; self.reject = nil; } } - (void)handleAppBecomingActive { [[NSNotificationCenter defaultCenter] removeObserver:self]; if (self.resolve) { self.resolve([self getPermissions]); self.resolve = nil; self.reject = nil; } } - (void)handleAppBecomingInactive { // Let's wait until the app becomes inactive - this happens when OS displays the // permission dialog - then we can cancel the timeout handler. _isWaitingForTimeout = false; [[NSNotificationCenter defaultCenter] removeObserver:self]; // When the app is inactive it means that a permission dialog is showing and we should ask to be // notified when the dialog is closed: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppBecomingActive) name:UIApplicationDidBecomeActiveNotification object:nil]; } - (void)setupAppInactivateTimeout { _isWaitingForTimeout = true; // Obtain a reference to the current queue dispatch_queue_t currentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); // Calculate the time for the delay dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)); EX_WEAKIFY(self); // Schedule the block to be executed after the delay dispatch_after(delayTime, currentQueue, ^{ EX_ENSURE_STRONGIFY(self) // Check if we are still waiting - ie. we haven't seen a permission dialog if (self.isWaitingForTimeout && self.resolve) { self.isWaitingForTimeout = false; self.resolve([self getPermissions]); self.resolve = nil; self.reject = nil; } }); } - (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus { EXPermissionStatus status; switch (systemStatus) { case kCLAuthorizationStatusAuthorizedAlways: { status = EXPermissionStatusGranted; break; } case kCLAuthorizationStatusDenied: case kCLAuthorizationStatusRestricted: { status = EXPermissionStatusDenied; break; } case kCLAuthorizationStatusAuthorizedWhenInUse: { if (_wasAsked) { status = EXPermissionStatusDenied; } else { status = EXPermissionStatusUndetermined; } break; } case kCLAuthorizationStatusNotDetermined: default: { status = EXPermissionStatusUndetermined; break; } } return @{ @"status": @(status), @"scope": @(systemStatus == kCLAuthorizationStatusAuthorizedWhenInUse ? "whenInUse" : systemStatus == kCLAuthorizationStatusAuthorizedAlways ? "always" : "none") }; } @end