Skip to content

Commit

Permalink
Implement the fixFitnessPermissions method
Browse files Browse the repository at this point in the history
High level changes:
- create a new method in the foreground delegate
    - checks and returns appropriate plugin results
    - moved from `TripDiarySensorControlChecks`
- create a new interface for the async operation to read the activity results,
  which is the only way to generate the prompt
    - moved this from the `cordova-server-sync` repo
        e-mission/cordova-server-sync#50
- stored callback functions and invoked them asynchronously, similar to the
  location changes in ca979bd
- ensured that the simulator always returns TRUE to allow easy testing of other features
- changed the error messages to improve user instructions, including:
    - if motion activity is not supported on the device, uninstall
    - if the permission is off, fix in app settings
    - if the setting is off, fix and restart the app

Related testing:
e-mission/e-mission-docs#680 (comment)
e-mission/e-mission-docs#680 (comment)
  • Loading branch information
shankari committed Feb 16, 2022
1 parent c2c58be commit f3c673a
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 86 deletions.
6 changes: 3 additions & 3 deletions res/ios/en.lproj/DCLocalizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"new-data-collections-terms" = "New data collection terms - collection paused until consent";
"error-reading-activities" = "Error while reading activities";
"travel-mode-unavailable" = "Travel mode detection may be unavailable.";
"activity-detection-unsupported" = "Activity detection unsupported";
"activity-permission-problem" = "No 'Motion & Fitness' permission - automatic mode detection will not work. Turn it on (Settings -> app)";
"activity-turned-off-problem" = "Motion & Fitness Service disabled - automatic mode detection will not work. Turn it on (Settings -> Privacy)";
"activity-detection-unsupported" = "Activity detection unsupported. Please uninstall.";
"activity-permission-problem" = "'Motion & Fitness' permission off, please fix in app settings";
"activity-turned-off-problem" = "Motion & Fitness Service disabled. Turn it on (Phone Settings -> Privacy) and *restart the app*";
"travel-mode-unknown" = "Travel mode detection unavailable - all trips will be UNKNOWN.";
"bad-loc-tracking-problem" = "Background location accuracy is consistently poor - trip tracking may not work. Report problem.";
"location-turned-off-problem" = "Location Services are turned off. Turn it on (Settings -> Privacy)";
Expand Down
2 changes: 2 additions & 0 deletions src/ios/BEMDataCollection.m
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ - (void)isValidLocationPermissions:(CDVInvokedUrlCommand*)command

- (void)fixFitnessPermissions:(CDVInvokedUrlCommand*)command
{
[[[SensorControlForegroundDelegate alloc] initWithDelegate:self.commandDelegate
forCommand:command] checkAndPromptFitnessPermissions];
}

- (void)isValidFitnessPermissions:(CDVInvokedUrlCommand*)command
Expand Down
10 changes: 10 additions & 0 deletions src/ios/Verification/SensorControlForegroundDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
- (void) checkAndPromptLocationSettings;
- (void) checkAndPromptLocationPermissions;
- (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status;

- (void) checkAndPromptFitnessPermissions;
- (void) didRecieveFitnessPermission:(BOOL)isPermitted;
@end

@interface TripDiaryDelegate (TripDiaryDelegatePermissions)
Expand All @@ -25,3 +28,10 @@
didChangeAuthorizationStatus:(CLAuthorizationStatus)status;

@end

@interface MotionActivityPermissionDelegate: NSObject
+ (void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate;
+ (void)readAndPromptForPermission;

@end

127 changes: 127 additions & 0 deletions src/ios/Verification/SensorControlForegroundDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,80 @@ - (void) didChangeAuthorizationStatus:(CLAuthorizationStatus)status

}

- (void)checkAndPromptFitnessPermissions {
NSString* callbackId = [command callbackId];
#if TARGET_OS_SIMULATOR
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK];
[commandDelegate sendPluginResult:result callbackId:callbackId];
#else
@try {
if ([CMMotionActivityManager isActivityAvailable] == YES) {
[LocalNotificationManager addNotification:@"Motion activity available, checking auth status"];
CMAuthorizationStatus currAuthStatus = [CMMotionActivityManager authorizationStatus];
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Auth status = %ld", currAuthStatus]];

if (currAuthStatus == CMAuthorizationStatusAuthorized) {
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK];
[commandDelegate sendPluginResult:result callbackId:callbackId];
}

if (currAuthStatus == CMAuthorizationStatusNotDetermined) {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status not determined, initializing to get regular prompt"]];
[MotionActivityPermissionDelegate registerForegroundDelegate:self];
[MotionActivityPermissionDelegate readAndPromptForPermission];
}

if (currAuthStatus == CMAuthorizationStatusRestricted) {
/*
It looked like this status is read when the app starts and cached after that. This is not resolvable from the code, so we just change the resulting message to highlight that the app needs to be restarted.
Gory details at: https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1040948835
*/
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection not enabled, prompting user to change Settings"]];
NSString* msg = NSLocalizedStringFromTable(@"activity-turned-off-problem", @"DCLocalizable", nil);
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_ERROR
messageAsString:msg];
[commandDelegate sendPluginResult:result callbackId:callbackId];
}


if ([CMMotionActivityManager authorizationStatus] == CMAuthorizationStatusDenied) {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status denied, opening app settings to enable"]];

NSString* msg = NSLocalizedStringFromTable(@"activity-permission-problem", @"DCLocalizable", nil);
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_ERROR
messageAsString:msg];
[commandDelegate sendPluginResult:result callbackId:callbackId];
[self openAppSettings];
}
} else {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection unsupported, all trips will be UNKNOWN"]];
NSString* msg = NSLocalizedStringFromTable(@"activity-detection-unsupported", @"DCLocalizable", nil);
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_ERROR
messageAsString:msg];
[commandDelegate sendPluginResult:result callbackId:callbackId];
}
}
@catch (NSException *exception) {
NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception];
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_ERROR
messageAsString:msg];
[commandDelegate sendPluginResult:result callbackId:callbackId];
}
#endif
}

-(void) didRecieveFitnessPermission:(BOOL)isPermitted
{
[self sendCheckResult:isPermitted
errorKey:@"activity-permission-problem"];
}

-(void)promptForPermission:(CLLocationManager*)locMgr {
if (IsAtLeastiOSVersion(@"13.0")) {
NSLog(@"iOS 13+ detected, launching UI settings to easily enable always");
Expand All @@ -179,6 +253,7 @@ -(void)promptForPermission:(CLLocationManager*)locMgr {
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
Expand Down Expand Up @@ -238,5 +313,57 @@ - (void)locationManager:(CLLocationManager *)manager
[SensorControlBackgroundChecker checkAppState];
}
}
@end

@implementation MotionActivityPermissionDelegate
NSMutableArray* foregroundDelegateList;

/*
* This is a bit tricky since this function is called whenever the authorization is changed
* Design decisions are at:
* https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035972636
* https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035976420
* https://github.com/e-mission/e-mission-docs/issues/680#issuecomment-1035984060
*/

+(void)registerForegroundDelegate:(SensorControlForegroundDelegate*) foregroundDelegate
{
if (foregroundDelegateList == nil) {
foregroundDelegateList = [NSMutableArray new];
}
[foregroundDelegateList addObject:foregroundDelegate];
}

+(void)readAndPromptForPermission {
CMMotionActivityManager* activityMgr = [[CMMotionActivityManager alloc] init];
NSOperationQueue* mq = [NSOperationQueue mainQueue];
NSDate* startDate = [NSDate new];
NSTimeInterval dayAgoSecs = 24 * 60 * 60;
NSDate* endDate = [NSDate dateWithTimeIntervalSinceNow:-(dayAgoSecs)];
/* This queryActivity call is the one that prompt the user for permission */
[activityMgr queryActivityStartingFromDate:startDate toDate:endDate toQueue:mq withHandler:^(NSArray *activities, NSError *error) {
if (error == nil) {
[LocalNotificationManager addNotification:@"activity recognition works fine"];
if (foregroundDelegateList.count > 0) {
for (id currDelegate in foregroundDelegateList) {
[currDelegate didRecieveFitnessPermission:TRUE];
}
[foregroundDelegateList removeAllObjects];
} else {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"no foreground delegate callbacks found for fitness sensors, ignoring success..."]];
}
} else {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Error %@ while reading activities, travel mode detection may be unavailable", error]];
if (foregroundDelegateList.count > 0) {
for (id currDelegate in foregroundDelegateList) {
[currDelegate didRecieveFitnessPermission:FALSE];
}
[foregroundDelegateList removeAllObjects];
} else {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"no foreground delegate callbacks found for fitness sensor error %@, ignoring...", error]];
}
}
}];
}

@end
91 changes: 8 additions & 83 deletions src/ios/Verification/TripDiarySensorControlChecks.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@ +(BOOL)checkLocationPermissions {
}

+(BOOL)checkMotionActivitySettings {
#if TARGET_OS_SIMULATOR
return TRUE;
#else
return [CMMotionActivityManager isActivityAvailable] == YES;
#endif
}

+(BOOL)checkMotionActivityPermissions {
#if TARGET_OS_SIMULATOR
return TRUE;
#else
CMAuthorizationStatus currAuthStatus = [CMMotionActivityManager authorizationStatus];
return currAuthStatus == CMAuthorizationStatusAuthorized;
#endif
}

+(BOOL)checkNotificationsEnabled {
Expand All @@ -48,87 +56,4 @@ +(UIUserNotificationSettings*) REQUESTED_NOTIFICATION_TYPES {
categories:nil];
}


/*
+(void)checkSettingsAndPermission {
[TripDiarySettingsCheck checkLocationSettingsAndPermission:TRUE];
[TripDiarySettingsCheck checkMotionSettingsAndPermission:TRUE];
}
+(void)checkMotionSettingsAndPermission:(BOOL)inBackground {
if ([CMMotionActivityManager isActivityAvailable] == YES) {
[LocalNotificationManager addNotification:@"Motion activity available, checking auth status"];
CMAuthorizationStatus currAuthStatus = [CMMotionActivityManager authorizationStatus];
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Auth status = %ld", currAuthStatus]];
if (currAuthStatus == CMAuthorizationStatusRestricted) {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection not enabled, prompting user to change Settings"]];
if (inBackground) {
NSString* errorDescription = NSLocalizedStringFromTable(@"activity-turned-off-problem", @"DCLocalizable", nil);
[LocalNotificationManager showNotificationAfterSecs:errorDescription withUserInfo:NULL secsLater:60];
}
[TripDiarySettingsCheck showLaunchSettingsAlert:@"activity-detection-unsupported" withMessage:@"activity-turned-off-problem" button:@"fix-service-action-button"];
}
if (currAuthStatus == CMAuthorizationStatusNotDetermined) {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status not determined, initializing to get regular prompt"]];
[BEMActivitySync initWithConsent];
}
if ([CMMotionActivityManager authorizationStatus] == CMAuthorizationStatusDenied) {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity status denied, opening app settings to enable"]];
NSString* errorDescription = NSLocalizedStringFromTable(@"activity-permission-problem", @"DCLocalizable", nil);
if (inBackground) {
[LocalNotificationManager showNotificationAfterSecs:errorDescription withUserInfo:NULL secsLater:60];
}
[TripDiarySettingsCheck showLaunchSettingsAlert:@"permission-problem-title" withMessage:@"activity-permission-problem" button:@"fix-permission-action-button"];
}
} else {
[LocalNotificationManager addNotification:[NSString stringWithFormat:@"Activity detection unsupported, all trips will be UNKNOWN"]];
NSString* title = NSLocalizedStringFromTable(@"activity-detection-unsupported", @"DCLocalizable", nil);
NSString* message = NSLocalizedStringFromTable(@"travel-mode-unknown", @"DCLocalizable", nil);
UIAlertController* alert = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
}];
[alert addAction:defaultAction];
[TripDiarySettingsCheck showSettingsAlert:alert];
}
}
+(void)showLaunchSettingsAlert:(NSString*)titleTag withMessage:(NSString*)messageTag button:(NSString*)buttonTag {
NSString* title = NSLocalizedStringFromTable(titleTag, @"DCLocalizable", nil);
NSString* message = NSLocalizedStringFromTable(messageTag, @"DCLocalizable", nil);
NSString* errorAction = NSLocalizedStringFromTable(buttonTag, @"DCLocalizable", nil);
UIAlertController* alert = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:errorAction style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[TripDiarySettingsCheck openAppSettings];
}];
[alert addAction:defaultAction];
[TripDiarySettingsCheck showSettingsAlert:alert];
}
+(void) openAppSettings {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:^(BOOL success) {
if (success) {
NSLog(@"Opened url");
} else {
NSLog(@"Failed open");
}}];
}
+(void) showSettingsAlert:(UIAlertController*)alert {
CDVAppDelegate *ad = [[UIApplication sharedApplication] delegate];
CDVViewController *vc = ad.viewController;
[vc presentViewController:alert animated:YES completion:nil];
}
*/

@end

0 comments on commit f3c673a

Please sign in to comment.