1

I've implemented a progressive Location permission request using Geolocator as I need background location for my app and both Android and iOS platforms require it to be asked only after being granted the while in use permission. The problem is that while on Android it all works as expected and the second time I request permission with a Prominent Disclosure it opens the Location Permission screen, on iOS is not showing a second pop-up asking to change the permission to always allow and just returns the LocationPermission.whileInUse status. Tried both on iPhone6 running iOS 12.5 and Simulator running iOS 16 but the second system popup doesn't appear. I'm I wrong expecting to see a second system popup when requesting permission a second time?

Here are the prints from the console:

// at start
flutter: LocationBloc.getLocationPermission value is denied

// at first request system popup appears
flutter: LocationBloc._requestLocationPermission value is whileInUse

// at second request system popup doesn't appear
flutter: TrackingRepository.getLocationPermission() LocationPermission is: LocationPermission.whileInUse
flutter: TrackingBloc._getBackgroundLocationPermission value is whileInUse
flutter: TrackingRepository.requestLocationPermission() LocationPermission is: LocationPermission.whileInUse
flutter: TrackingBloc._requestLocationPermission value is whileInUse

This is the method used to request permission:

Future<String> requestLocationPermission() async {
    return await locationManager.checkPermission().then((value) async {
      late String permission;
      if (value != LocationPermission.always) {
        permission =
            await locationManager.requestPermission().then((value) async {
          print(
              'TrackingRepository.requestLocationPermission() LocationPermission is: $value');
          switch (value) {
            case LocationPermission.denied:
              return 'denied';
            case LocationPermission.deniedForever:
              return 'deniedForever';
            case LocationPermission.whileInUse:
              return 'whileInUse';
            case LocationPermission.always:
              return 'always';
            case LocationPermission.unableToDetermine:
              return 'unableToDetermine';
          }
        }).catchError((e) {
          print('TrackingRepository.requestLocationPermission() error: $e');
        });
      }
      return permission;
    });
  }

I have set Deployment target: 12.4, added two entries in info.plist as

<key>NSLocationWhenInUseUsageDescription</key>
<string>fixit needs you position to enable its functionalities</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>fixit needs your position to enable you to track your routes even when is in background</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>fixit needs your position to enable you to track your routes even when is in background</string>
<key>UIBackgroundModes</key>
    <array>
        <string>fetch</string>
        <string>location</string>
        <string>processing</string>
        <string>remote-notification</string>
    </array>

I also added the Location updates in Signing & Capabilities/Background Modes.

This is the location stream method, which differentiate between Androidand iOS settings:

Stream<Position> startTracking() {
    _positionStreamController = new StreamController<Position>.broadcast();

    locationManager.checkPermission().then((value) async {
      if (value == LocationPermission.denied ||
          value == LocationPermission.deniedForever ||
          value == LocationPermission.whileInUse) {
        await locationManager.requestPermission().then((value) {
          print(
              'TrackingRepository.startTracking() requestPermission is: $value');
        }).catchError((e) {
          print(
              'TrackingRepository.startTracking() requestPermission error: $e');
        });
      }
    }).catchError((e) {
      print('TrackingRepository.startTracking() error: $e');
    });

    late var locationSettings = Platform.isIOS
        ? AppleSettings(
            accuracy: LocationAccuracy.bestForNavigation,
            allowBackgroundLocationUpdates: true,
            showBackgroundLocationIndicator: true,
            activityType: ActivityType.otherNavigation)
        : LocationSettings(accuracy: LocationAccuracy.best, distanceFilter: 0);

    _positionSubscription = locationManager
        .getPositionStream(locationSettings: locationSettings)
        .listen((event) {
      _positionStreamController.sink.add(event);
    });

    return _positionStreamController.stream;
  }

Now, as I'm trying it on an iPhone 6 with iOS 12.5 I only see Accept and Deny options at system popup, but I was expecting also an Always allow option, so I have to manually choose it otherwise background position updates are not received. Isn't supposed to appear a second system popup to allow changing the permissions?

Am I missing out some settings? Many thanks.

Vincenzo
  • 5,304
  • 5
  • 38
  • 96
  • What location permission are you requesting? Always or when in use? You need to request always and then actually make use of background location. Also, the escalation from when in use to always via provisional always was introduced in iOS 13 so you won't see it on iOS 12.5. You really need to be testing on a more modern version of iOS. iOS 12 probably represents much less than 1% of active iOS users. – Paulw11 Feb 23 '23 at 17:06
  • @Paulw11 hi again, I know, my poor iPhone6 needs retiring.. on it I only see `Allow` (which selects `whileInUse`) and `Deny` options, but on Simulator running iOS 16.2 I see and choose `Allow While Using App` which also selects `whileInUse`. Then later on in the app when I request permission again to change it to `always` the system popup just won't show up, the methods just returns `whileInUse` again. I added the console outputs to show the two stages – Vincenzo Feb 23 '23 at 17:28
  • 1
    If you look in settings for your app in the simulator do you see the option there to enable "always"? If so then your code is requesting "always". If not then it is only requesting when in use, I looke at the code for this framework and it decides whether to request always or when in use based in the entries in your info.plist. You do have the right entries but I don't like this approach very much to be honest. It would have been better if they had let you explicitly state what permission you wanted to request. – Paulw11 Feb 23 '23 at 19:53
  • @Paulw11 Thank you, you put me in the right direction. It seems that `iOS` needs to be asked for specific location permission, which `Geolocator` does not allow to. I tried `permission_handler` which let you have more fine grained control over what permission you want to check the status of or request and it did work. – Vincenzo Feb 25 '23 at 14:20

1 Answers1

1

After quite a few tries (all failed) to open up the second system popup for requesting the always permission, I started thinking that as commented by Paulw11, possibly the problem was the generic location permission request that Geolocator does, so I decided to try permission_handler plugin (just for iOS to start) which does allow to check the status and request specific location permission types. It did work..it now finally does open the second system popup so user can change the permission to always, on iOS (on both the Simulator and my soon to be retired iPhone6) as well as on Android platforms. That general location permission might actually be the real cause for iOS not showing a second system popup as even with permission_handler if I use Permission.location.request() no system popup gets shown while Permission.locationWhenInUse.request() does. Here's the working code:

Location

/// Using permission_handler for both platforms
  Future<String> getLocationPermission() async {
    print('\n\nLocationRepository.getLocationPermission() started\n\n');
    late String permission;
    permission = await Permission.locationWhenInUse.status.then((value) {
      print(
          '\n\n LocationRepository.getLocationPermission Permission.locationWhenInUse.status is ${value.name}');

      // permission = await Permission.location.status.then((value) {
      //   print(
      //       '\n\n LocationRepository.getLocationPermission Permission.location.status is ${value.name}');
      switch (value) {
        case PermissionStatus.denied:
          return 'denied';
        case PermissionStatus.permanentlyDenied:
          return 'deniedForever';
        case PermissionStatus.limited:
          return 'limited';
        case PermissionStatus.granted:
          return 'granted';
        case PermissionStatus.restricted:
          return 'restricted';
      }
    });
    return permission;
  }

  Future<String> requestLocationPermission() async {
    print('LocationRepository.requestLocationPermission started');
    late String permission;
    // general location doesn't open the popup
    // var status = await Permission.location.status;
    // print('Permission.location.status is $status');
    var status = await Permission.locationWhenInUse.status;
    print('Permission.locationWhenInUse.status is $status');

    /// NOT Granted
    if (!status.isGranted) {
      // var status = await Permission.location.request();
      // print('Permission.location.request() status is $status');
      var status = await Permission.locationWhenInUse.request();
      print('Permission.locationWhenInUse.request() status is $status');
      if (status.isGranted) {
        permission = 'granted';
      } else {
        permission = 'denied';
      }
    }

    /// Granted
    else {
      permission = 'granted';
    }
    return permission;
  }

Bg location

/// Using permission_handler for both
  Future<String> getLocationPermissionStatus() async {
    print('\n\nTrackingRepository.getLocationPermissionStatus() started\n\n');
    late String permission;
    permission = await Permission.locationAlways.status.then((value) {
      print(
          'TrackingRepository.getLocationPermissionStatus() Permission.locationAlways.status is: ${value.name}\n\n');
      switch (value) {
        case PermissionStatus.denied:
          return 'denied';
        case PermissionStatus.permanentlyDenied:
          return 'deniedForever';
        case PermissionStatus.limited:
          return 'limited';
        case PermissionStatus.granted:
          return 'granted';
        case PermissionStatus.restricted:
          return 'restricted';
      }
    });
    return permission;
  }

  Future<String> requestLocationPermission() async {
    late String permission;
    var locationWhenInUseStatus =
        await Permission.locationWhenInUse.status.then((value) {
      print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
          'Permission.locationWhenInUse.status is: ${value.name}');
      return value;
    });

    /// locationWhenInUseStatus NOT Granted
    if (!locationWhenInUseStatus.isGranted) {
      print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
          'locationWhenInUseRequest NOT Granted, we now request it');

      /// Ask locationWhileInUse permission
      var locationWhenInUseRequest =
          await Permission.locationWhenInUse.request();
      print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
          'Permission.locationWhenInUse.request() status is: $locationWhenInUseRequest');

      /// locationWhenInUseRequest granted
      if (locationWhenInUseRequest.isGranted) {
        /// When in use NOW Granted
        print('\n\nTrackingRepository.requestLocationPermission() ios\n'
            'When in use NOW Granted');
        permission = 'whileInUse';
        PermissionStatus status = await Permission.locationAlways.request();
        print(
            '\n\nTrackingRepository.requestLocationPermission() ios locationWhenInUse is Now Granted\n'
            'Permission.locationAlways.request() status is: $status');

        if (status.isGranted) {
          /// Always is NOW Granted
          print('\n\nTrackingRepository.requestLocationPermission() ios\n'
              'Always use NOW Granted');
          permission = 'granted';
          print(
              '\n\nTrackingRepository.requestLocationPermission() ios locationAlways is Now Granted\n'
              'Permission.locationAlways.request() status is: $status');
        } else {
          //Do another stuff
        }
      }

      /// locationWhenInUseRequest not granted
      else {
        //The user deny the permission
        permission = 'denied';
      }
      if (locationWhenInUseRequest.isPermanentlyDenied) {
        //When the user previously rejected the permission and select never ask again
        //Open the screen of settings
        print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
            'Permission.locationWhenInUse.request is isPermanentlyDenied');
        permission = 'deniedForever';
        bool didOpen = await openAppSettings();
        print(
            '\n\nTrackingRepository.requestLocationPermission() ios isPermanentlyDenied\n'
            'openAppSettings() didOpen: $didOpen');

        // TODO: re-check for locationWhenInUse permission status?
      }
    }

    /// locationWhenInUseStatus is ALREADY Granted
    else {
      print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
          'locationWhenInUse ALREADY Granted, we now check for locationAlways permission');
      permission = 'whenInUse';

      var locationAlwaysStatus =
          await Permission.locationAlways.status.then((value) {
        print(
            '\n\nTrackingRepository.requestLocationPermission() iOS\nlocationWhenInUse already granted\n'
            'Permission.locationAlways.status is: ${value.name}');
        return value;
      });

      /// locationAlways is NOT Already Granted
      if (!locationAlwaysStatus.isGranted) {
        print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
            'locationAlways not granted, we now ask for permission');

        /// ask locationAlways permission
        var locationAlwaysStatus = await Permission.locationAlways.request();

        /// finally it opens the system popup
        print('\n\nTrackingRepository.requestLocationPermission() iOs\n'
            'Permission.locationAlways.request() status is: $locationAlwaysStatus');

        /// locationAlways is NOW Granted
        if (locationAlwaysStatus.isGranted) {
          print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
              'locationAlways was Granted upon request');
          permission = 'granted';
        }

        /// locationAlways was NOT Granted
        else {
          print('\n\nTrackingRepository.requestLocationPermission() iOS\n'
              'Permission.locationAlways.request() status was NOT Granted upon request, we now open AppSettings');
          await openAppSettings().then((value) {
            print(
                '\n\nTrackingRepository.requestLocationPermission() ios locationAlways isPermanentlyDenied\n'
                'openAppSettings() didOpen: $value');
          });
          // TODO: re-check locationAlways permission status??
        }
      }

      /// locationAlways is ALREADY Granted
      else {
        permission = 'granted';
      }
    }
    return permission;
  }
Vincenzo
  • 5,304
  • 5
  • 38
  • 96