From 987a432a478a3bdecd5425698ac71c661add6b05 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 4 Oct 2024 17:02:20 -0400 Subject: [PATCH] implement "2 step" approach for location perms on iOS 13.4+ https://github.com/e-mission/e-mission-docs/issues/1094#issue-2564427585 If on iOS 13.4 or higher, we will first ask for 'whenInUse' authorization. If the user accepts this, we will receive the authorization status change in didChangeAuthorizationStatus; at which point we will attempt to trigger another request, this time for 'always', which should show the user a second prompt. If that fails, we will navigate to the app settings. This approach involved re-registering the foreground delegate from within that foreground delegate's handler, so I adjusted the way the delegate list is cleared, so as to not clear out the new additions. Also adjusted the strings in plugin.xml to be more descriptive and transparent about what permissions are needed for tracking, as well as when and why. --- plugin.xml | 10 ++-- .../SensorControlForegroundDelegate.m | 57 +++++++++++++------ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/plugin.xml b/plugin.xml index 5904d24..a2eb9dc 100644 --- a/plugin.xml +++ b/plugin.xml @@ -212,19 +212,19 @@ - We use your data to create an automatic trip diary + OpenPATH requires "Precise" location access allowed "Always" to create an automatic trip diary - We use your data to create an automatic trip diary + OpenPATH also requires "Always" location access to track your trips in the background - We use your data to create an automatic trip diary + OpenPATH requires "Precise" location access allowed "While Using App" to create an automatic trip diary - Our app uses the motion sensors to determine the transportation mode for the sections of your trip + OpenPATH uses your phone's motion sensors to determine the modes of transportation during your trips @@ -232,7 +232,7 @@ - We need Bluetooth access to interact with BLE beacons for the fleet version of the app. + OpenPATH requires Bluetooth access to interact with BLE beacons for the fleet version of the app diff --git a/src/ios/Verification/SensorControlForegroundDelegate.m b/src/ios/Verification/SensorControlForegroundDelegate.m index 3de0c7e..082d4a0 100644 --- a/src/ios/Verification/SensorControlForegroundDelegate.m +++ b/src/ios/Verification/SensorControlForegroundDelegate.m @@ -153,6 +153,16 @@ - (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; [commandDelegate sendPluginResult:result callbackId:callbackId]; + } else if (status == kCLAuthorizationStatusAuthorizedWhenInUse) { + if (IsAtLeastiOSVersion(@"13.4")) { + NSLog(@"iOS >=13.4 detected and 'whenInUse' authorized, need second step to request 'always'"); + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [[TripDiaryStateMachine instance].locMgr requestAlwaysAuthorization]; + if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateInactive) { + NSLog(@"App is active, i.e. request did not launch (happens if the user chose 'Allow Once' in step 1). Launching app settings to manually enable 'Always'"); + [self openAppSettings]; + } + } } else { [LocalNotificationManager addNotification:[NSString stringWithFormat:@"status %d != always %d, returning error", status, kCLAuthorizationStatusAuthorizedAlways]]; NSString* msg = NSLocalizedStringFromTable(@"location_permission_off_app_open", @"DCLocalizable", nil); @@ -281,25 +291,39 @@ - (void) didRegisterUserNotificationSettings:(UIUserNotificationSettings*)newSet } -(void)promptForPermission:(CLLocationManager*)locMgr { - if (IsAtLeastiOSVersion(@"13.0")) { - NSLog(@"iOS 13+ detected, launching UI settings to easily enable always"); + if (IsAtLeastiOSVersion(@"13.4")) { + NSLog(@"iOS >=13.4 detected, using two-step approach of 'when in use' first and then 'always'"); + if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) { + NSLog(@"Current location authorization = %d, when in use = %d, requesting when in use", + [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedWhenInUse); + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [locMgr requestWhenInUseAuthorization]; + } else if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse) { + NSLog(@"Current location authorization = %d, always = %d, requesting always", + [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedAlways); + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [locMgr requestAlwaysAuthorization]; + if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateInactive) { + NSLog(@"App is active, i.e. request did not launch (happens if the user chose 'Allow Once' in step 1). Launching app settings to manually enable 'Always'"); + [self openAppSettings]; + } + } else { + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [self openAppSettings]; + } + } else if (IsAtLeastiOSVersion(@"13.0")) { + NSLog(@"iOS >=13,<13.4 detected, launching UI settings to manually enable 'always'"); // we want to leave the registration in the prompt for permission, since we don't want to register callbacks when we open the app settings for other reasons [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; [[TripDiaryStateMachine instance].locMgr startUpdatingLocation]; [self openAppSettings]; - } - else { + } else { + NSLog(@"iOS <13 detected, simply requesting 'always'"); if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) { - if ([CLLocationManager instancesRespondToSelector:@selector(requestAlwaysAuthorization)]) { - NSLog(@"Current location authorization = %d, always = %d, requesting always", - [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedAlways); - [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; - [locMgr requestAlwaysAuthorization]; - } else { - // TODO: should we remove this? Not sure when it will ever be called, given that - // requestAlwaysAuthorization is available in iOS8+ - [LocalNotificationManager addNotification:@"Don't need to request authorization, system will automatically prompt for it"]; - } + NSLog(@"Current location authorization = %d, always = %d, requesting always", + [CLLocationManager authorizationStatus], kCLAuthorizationStatusAuthorizedAlways); + [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; + [locMgr requestAlwaysAuthorization]; } else { // we want to leave the registration in the prompt for permission, since we don't want to register callbacks when we open the app settings for other reasons [[TripDiaryStateMachine delegate] registerForegroundDelegate:self]; @@ -346,11 +370,12 @@ - (void)locationManager:(CLLocationManager *)manager if (foregroundDelegateList.count > 0) { [LocalNotificationManager addNotification:[NSString stringWithFormat:@"%lu foreground delegates found, calling didChangeAuthorizationStatus to return the new value %d", (unsigned long)foregroundDelegateList.count, status]]; + int originalDelegateCount = (int)foregroundDelegateList.count; for (id currDelegate in foregroundDelegateList) { [currDelegate didChangeAuthorizationStatus:(CLAuthorizationStatus)status]; } - [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Notified all foreground delegates, removing all of them"]]; - [foregroundDelegateList removeAllObjects]; + [LocalNotificationManager addNotification:[NSString stringWithFormat:@"Notified all foreground delegates, removing %d delegates", originalDelegateCount]]; + [foregroundDelegateList removeObjectsInRange:NSMakeRange(0, originalDelegateCount)]; } else { [LocalNotificationManager addNotification:[NSString stringWithFormat:@"No foreground delegate found, calling SensorControlBackgroundChecker from didChangeAuthorizationStatus to verify location service status and permission"]]; [SensorControlBackgroundChecker checkAppState];