Baseflow / Baseflow/flutter-permission-handler

[Bug]: iOS - Permission.location.status / .isGranted can hang the main thread for 2+ seconds

Open
#1,557 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Dart
Stars
2.2k
Forks
970
Avg merge
14h 22m
Merged PRs (30d)
2

Description

### Please check the following before submitting a new issue.

- [x] I have searched the [existing issues](https://github.com/baseflow/flutter-permission-handler/issues).
- [x] I have carefully [read the documentation](https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler/README.md) and verified I have added the required platform specific configuration.

### Please select affected platform(s)

- [ ] Android
- [x] iOS
- [ ] Windows

### Steps to reproduce

We collect production crash/ANR data via Sentry (App Hang detection, watchdog
threshold 2000ms). We see repeated "App Hanging" events on iOS whose main
thread stack trace is:

```
App Hanging: App hanging for at least 2000 ms.

Runner 0x100794114 +[LocationPermissionStrategy permissionStatus:] (LocationPermissionStrategy.m:166)
Runner 0x100792ec0 +[PermissionManager checkPermissionStatus:result:] (PermissionManager.m:23)
Runner 0x100792a70 -[PermissionHandlerPlugin handleMethodCall:result:] (PermissionHandlerPlugin.m:30)
Flutter 0x101a7dce4 __45-[FlutterMethodChannel setMethodCallHandler:]_block_invoke (FlutterChannels.mm:315)
Flutter 0x1015dbdf0 flutter::PlatformMessageHandlerIos::HandlePlatformMessage (platform_message_handler_ios.mm:70)
```

This happens most frequently right at `applicationDidBecomeActive` /
`AppLifecycleState.resumed`, when app code calls `Permission.location.status`
or `Permission.location.isGranted` to detect whether the user changed the
location permission in Settings while the app was backgrounded.

Root cause: `LocationPermissionStrategy.checkPermissionStatus:` calls
`+[CLLocationManager authorizationStatus]` synchronously on whatever thread
invoked the method channel (the main/platform thread by default), with no
`dispatch_async` off-thread hop.

This is the same class of problem as #1002, which was fixed for
`checkServiceStatus` (`CLLocationManager.locationServicesEnabled`) in #1329 /
`permission_handler_apple` 9.4.5:

```objc
- (void)checkServiceStatus:(PermissionGroup)permission completionHandler:(ServiceStatusHandler)completionHandler {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
BOOL isEnabled = [CLLocationManager locationServicesEnabled];
dispatch_async(dispatch_get_main_queue(), ^(void) {
completionHandler(isEnabled ? ServiceStatusEnabled : ServiceStatusDisabled);
});
});
}
```

However, checkPermissionStatus: (used by .status / .isGranted) was never given the same treatment and is still synchronous today in the latest published version (9.6.1):

```
- (PermissionStatus)checkPermissionStatus:(PermissionGroup)permission {
return [LocationPermissionStrategy permissionStatus:permission];
}

+ (PermissionStatus)permissionStatus:(PermissionGroup)permission {
CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus]; // synchronous, can block
...
}
```

Since PermissionManager.checkPermissionStatus:result: calls this synchronously and immediately passes the result to FlutterResult, the main thread is blocked for however long authorizationStatus takes internally (observed 2000ms+ on affected devices/iOS versions in production).

### Possible fix

Mirroring the checkServiceStatus fix, checkPermissionStatus could dispatch the location-specific call off the main thread and hop back before invoking the result callback, e.g. (scoped to location permission groups only, to avoid touching the other ~10 permission strategies that don't have this issue):

```
+ (void)checkPermissionStatus:(enum PermissionGroup)permission result:(FlutterResult)result {
id permissionStrategy = [PermissionManager permissionStrategy:permission];

if (permission == PermissionGroupLocation || permission == PermissionGroupLocationAlways ||
permission == PermissionGroupLocationWhenInUse) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
PermissionStatus status = [permissionStrategy checkPermissionStatus:permission];
dispatch_async(dispatch_get_main_queue(), ^{ result(@(status)); });
});
return;
}

PermissionStatus status = [permissionStrategy checkPermissionStatus:permission];
result(@(status));
}
```

### Expected results

Permission.location.status / .isGranted should never be able to block the calling (main) thread for multiple seconds, consistent with how serviceStatus already behaves after #1329.

### Actual results

Main thread hangs 2000ms+ inside [CLLocationManager authorizationStatus], reported by Sentry's App Hang detector on iOS, primarily right after the app becomes active again.

### Code sample

Code sample

```dart
[Paste your code here]
```

### Screenshots or video

Screenshots or video demonstration

[Upload media here]

### Version

permission_handler: 13.0.1 / permission_handler_apple: 9.6.1

### Flutter Doctor output

Doctor output

```console
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.44.8, on macOS 26.5.2 25F84 darwin-arm64, locale de-AT)
[✓] Android toolchain - develop for Android devices (Android SDK version 37.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 26.6)
[✓] Chrome - develop for the web
[✓] Connected device (2 available)
[✓] Network resources

• No issues found!
```

Contributor guide

Open the contributing guide

Research direction

Read LocationPermissionStrategy.m and PermissionManager.m, starting at checkPermissionStatus: and the existing checkServiceStatus asynchronous handling described in the issue. Verify the location status path no longer blocks the main thread and that the result callback still receives the permission status after the background check.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, ios, objective-c
Domain
mobile
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.