[Bug]: Requesting locationAlways does not await user's response.
Please check the following before submitting a new issue.
- [X] I have searched the existing issues.
- [X] I have carefully read the documentation and verified I have added the required platform specific configuration.
Please select affected platform(s)
- [ ] Android
- [X] iOS
- [ ] Windows
Steps to reproduce
- Check result of requesting locationWhenInUse permissions
await Permission.locationWhenInUse.request(). - Check result of requesting locationAlways permissions
await Permission.locationAlways.request().
Expected results
We expect the both requests to properly await the user's response to the iOS system dialog, and returns the user's choice.
Actual results
In the above steps, the first request properly awaits the user's response to the iOS system dialog, and returns the user's choice.
The second request immediately returns a response of not granted because the user hasn't responded yet to the dialog.
Code sample
Future<bool> _requestLocationWhileInUse() async {
// This actually waits for user to respond to dialog prompt before
// continuing to return the status.isGranted.
final status = await Permission.locationWhenInUse.request();
return status.isGranted;
}
Future<bool> _requestLocationAlways() async {
// This does not wait for user to respond to dialog prompt before
// continuing to return the status.isGranted, thus, we often receive
// false here, and have to check after the user responds to the dialog.
final status = await Permission.locationAlways.request();
return status.isGranted;
}
Screenshots or video
Screenshots or video demonstration
[Upload media here]
Version
10.4.3 - 11.0.0
Flutter Doctor output
Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.13.0, on macOS 13.5.1 22G90 darwin-arm64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.3)
[✓] VS Code (version 1.81.1)
[✓] Connected device (5 available)
[✓] Network resources
• No issues found!
Looks like a similar issue was previously filed around this, but marked as resolved: https://github.com/Baseflow/flutter-permission-handler/issues/1086 (=> https://github.com/Baseflow/flutter-permission-handler/pull/1089/files#r1254302197).
Same issue.
iOS 16.6.1, physical device
Pubspec.yaml permission_handler: ^10.4.5 permission_handler_apple: ^9.1.4
Podfile config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] 'PERMISSION_LOCATION=1', ]
Hi @jeffscaturro-aka, @niksen75,
Thank you for reporting this issue and providing detailed descriptions. I was able to reproduce the problem and did more detailed investigation.
Unfortunately the issue is not really easy to solve in our current architecture where we simply await the future when requesting permissions. The reason is that iOS will not always provide a status update when requesting "Always allow" permissions. Here are the situations:
User selected "Allow once"
When requesting location permissions and the user initially selected "Only once", requesting locationAlways will not trigger the permission dialog at all and no result is provided. Unfortunately Apple doesn't provide an API which informs us the user initially selected "Only once", instead the "When in use" permission is returned. This is also explained in Apple's documentation.
User selected "When in use" and follows up with "Keep When in use"
When the user selected "When in use" at the moment we initially request location permissions Apple will return the "When in use" permission. Now if the app requests location always permission the following happens:
- iOS will immediately call the
locationManager:didChangeAuthorizationStatus:callback with thekCLAuthorizationStatusAuthorizedWhenInUsestatus (this happens before the second permission dialog is shown). - Next iOS will show the permission dialog asking the user to "Keep using While in Use" or "Change to Allow Always".
- If the user selects "Keep using While in Use" iOS will close the dialog but will not inform us of any status updates.
User selected "When in use" and follows up with "Change to Allow always"
Again the user initially provided "When in use" permissions and the app is requesting location always permission:
- Before the second dialog is shown iOS will call the
locationManager:didChangeAuthorizationStatus:callback and confirms the current permission reporting thekCLAuthorizationStatusAuthorizedWhenInUsestatus. - iOS will show the permission dialog asking the user to "Keep using While in Use" or "Change to Allow Always".
- Only if the user select "Change to Allow Always", iOS will call the
locationManager:didChangeAuthorizationStatus:method and inform us of the newkCLAuthorizationStatusAuthorizedAlwaysstatus.
As you can see this makes it really hard (not to say impossible) to reliably await for the correct status when requesting location alway permission. This due to the fact that in several situations iOS will simply not inform us about the status when the user makes a selection.
We understand that the current solution provided by the plugin is not really helpful and therefore we are planning to do a refactor of the plugin where we will provide developers with the option to listen to a stream providing permission status changes instead of enforcing developers to await the request method for a status update. We will start the refactor very soon and hopefully can provide updates in the coming months. Until this time, we are not really able to provide a more solid solution and we hope you all understand.
We will keep this issue open to help track the refactor process and ensure this issue will be resolved as part of the refactor process.
P.S. alternative ideas are of course welcome.
@mvanbeusekom thank you so much for the triage and in depth explanation here, it is much appreciated and gives a great understanding of the limitations we're facing. I feel most developers can understand Apple doesn't always make it easy on us, especially when factoring in user privacy and permissions (rightfully so).
A workaround we had in place was simply utilizing a WidgetsBindingObserver where we were asking for this permission and listening to its lifecycle state changes (didChangeAppLifecycleState). I hope that helps some others who stumble across this issue, and of course, other ways around this would be welcomed to include on this thread!
For anyone reading this in the future, this is the didChangeAppLifecycleState workaround that people are mentioning:
class _LocationPageState extends State<LocationPage>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// this is used to listen to the app lifecycle state
// this function is called affter the OS location permission popup appears because
// the app is considered "paused" when that happens and "resumed" after the user
// has selected an option
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed && Platform.isIOS) {
print('resumed ios');
await handleIOSLocationWorkAround(context, bloc);
}
}
@Biowulf21 What is the exact workaround and what will your method handleIOSLocationWorkAround do?
I am just wondering as there is no connection between AppLifecycleState and this issue.
@Biowulf21 What is the exact workaround and what will your method
handleIOSLocationWorkArounddo? I am just wondering as there is no connection betweenAppLifecycleStateand this issue.
The app goes to suspend state when the location permission pop up appears and goes to resumed state when it's closed.