flutter-permission-handler icon indicating copy to clipboard operation
flutter-permission-handler copied to clipboard

[Bug]: Callbacks like onGrantedCallback does not work as expected.

Open ebsangam opened this issue 1 year ago • 17 comments

Please check the following before submitting a new issue.

Please select affected platform(s)

  • [X] Android
  • [ ] iOS
  • [ ] Windows

Steps to reproduce

  1. Use a callback Permission.contacts.onGrantedCallback
  2. Grant permission for camera.
  3. Permission.contacts.onGrantedCallback will be invoked.

Expected results

When using Permission.contacts.onGrantedCallback we expect this callback to invoke only if we grant permission for contacts.

Actual results

No mater what permission you add callback it will always gets invoked for every permission.

Code sample

Code sample
Permission.contacts.onGrantedCallback(
  () {
    print('Contacts permission granted.');
  },
);

Screenshots or video

Screenshots or video demonstration

[Upload media here]

Version

11.0.1

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.3, on macOS 14.6.1 23G93 darwin-arm64, locale en-NP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.95.3)
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

ebsangam avatar Dec 09 '24 04:12 ebsangam

Dear @ebsangam,

Can you elaborate a bit on the subject? Are you testing this in iOS or Android? And what version of OS?

Kind regards,

TimHoogstrate avatar Dec 11 '24 08:12 TimHoogstrate

I was testing on Android emulator SDK 34. I wanted to listen to contacts permission status change (on granted to be specific). So I used Permission.contacts.onGrantedCallback. But the callback gets invoked when I grant camera permission or microphone permission. Basically Permission.contacts.onGrantedCallback is not only listening to contacts permission updated but for every permission updates. Thanks.

ebsangam avatar Dec 11 '24 08:12 ebsangam

@TimHoogstrate

Running into the same issue. (Only tried building on Android so far, but I imagine all the platforms have this error.)

Here's a full flutter app code to replicate it:

import 'package:flutter/material.dart';

import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  PermissionStatus? locationState;
  PermissionStatus? notificationState;

  @override
  Widget build(BuildContext context) {
    const locationPermission = Permission.locationAlways;
    locationPermission.onDeniedCallback(() {
      setState(() => locationState = PermissionStatus.denied);
    });
    locationPermission.onGrantedCallback(() {
      setState(() => locationState = PermissionStatus.granted);
    });
    locationPermission.onLimitedCallback(() {
      setState(() => locationState = PermissionStatus.limited);
    });
    locationPermission.onPermanentlyDeniedCallback(() {
      setState(() => locationState = PermissionStatus.permanentlyDenied);
    });
    locationPermission.onProvisionalCallback(() {
      setState(() => locationState = PermissionStatus.provisional);
    });
    locationPermission.onRestrictedCallback(() {
      setState(() => locationState = PermissionStatus.restricted);
    });

    const notificationPermission = Permission.notification;
    notificationPermission.onDeniedCallback(() {
      setState(() => notificationState = PermissionStatus.denied);
    });
    notificationPermission.onGrantedCallback(() {
      setState(() => notificationState = PermissionStatus.granted);
    });
    notificationPermission.onLimitedCallback(() {
      setState(() => notificationState = PermissionStatus.limited);
    });
    notificationPermission.onPermanentlyDeniedCallback(() {
      setState(() => notificationState = PermissionStatus.permanentlyDenied);
    });
    notificationPermission.onProvisionalCallback(() {
      setState(() => notificationState = PermissionStatus.provisional);
    });
    notificationPermission.onRestrictedCallback(() {
      setState(() => notificationState = PermissionStatus.restricted);
    });

    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                for (final permissionEntry
                    in {
                      locationPermission: locationState,
                      notificationPermission: notificationState,
                    }.entries)
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text("${permissionEntry.key}: ${permissionEntry.value}"),
                      TextButton(
                        onPressed: () {
                          permissionEntry.key.request();
                        },
                        child: Text("REQUEST ${permissionEntry.key}"),
                      ),
                    ],
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Of course, you'll need to add the requisite permissions to Android manifest as well, and add permission_handler to pubspec.yaml as well:

<!-- Permissions options for the `access notification policy` group -->
    <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>

    <!-- Permissions options for the `notification` group -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <!-- Permissions options for the `location` group -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
  permission_handler: ^11.4.0

12people avatar Feb 24 '25 12:02 12people

Here's a recording of the odd behavior:

https://github.com/user-attachments/assets/440cd356-223f-4ec4-bdd6-672aad6e29d0

(Same code as I posted above.)

12people avatar Feb 24 '25 12:02 12people

As for my Android version, it's Android 13 (build 6.A.031.7).

12people avatar Feb 24 '25 12:02 12people

@12people,

Requesting the permission works fine (also in the example app), you can check it in the app settings after requesting and approving the permissions. However, your demo app is just presenting the results wrongly. Try to keep the registration of the callbacks outside of the build method.

@ebsangam I've verified that is works properly in the example app.

Kind regards,

TimHoogstrate avatar Mar 18 '25 08:03 TimHoogstrate

@12people,

Requesting the permission works fine (also in the example app), you can check it in the app settings after requesting and approving the permissions. However, your demo app is just presenting the results wrongly. Try to keep the registration of the callbacks outside of the build method.

@ebsangam I've verified that is works properly in the example app.

Kind regards,

I think you misunderstood the issue. Let's not focus on the example app instead my original issue I mentioned. It is the issue about callback that fires unnecessarily.

ebsangam avatar Mar 18 '25 08:03 ebsangam

Dear @ebsangam,

Still I cannot reproduce this. Please provide me with a clear sample. Again, I tested this but I cannot reproduce this. The sample of @12people is just not working properly. If I change any of the permissions from Permission.notification to another (for example contacts I get the same results (only then with .contacts).

Kind regards,

TimHoogstrate avatar Mar 18 '25 09:03 TimHoogstrate

I will provide you a minimal reproducible code when I am free.

ebsangam avatar Mar 18 '25 09:03 ebsangam

@TimHoogstrate

You're right, setting callbacks in the build method isn't a good idea. (I guess I was too focused on making a single-file example. In my real-world usage, I tried it with a Riverpod provider and didn't want to burden the example with that complexity.)

However, if I set the callbacks once in the initState method, I get the same result:

import 'package:flutter/material.dart';

import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {
  const MainApp({super.key});

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  static const _locationPermission = Permission.locationWhenInUse;
  static const _notificationPermission = Permission.notification;
  PermissionStatus? locationState;
  PermissionStatus? notificationState;

  @override
  void initState() {
    super.initState();
    _locationPermission.onDeniedCallback(() {
      setState(() => locationState = PermissionStatus.denied);
    });
    _locationPermission.onGrantedCallback(() {
      setState(() => locationState = PermissionStatus.granted);
    });
    _locationPermission.onLimitedCallback(() {
      setState(() => locationState = PermissionStatus.limited);
    });
    _locationPermission.onPermanentlyDeniedCallback(() {
      setState(() => locationState = PermissionStatus.permanentlyDenied);
    });
    _locationPermission.onProvisionalCallback(() {
      setState(() => locationState = PermissionStatus.provisional);
    });
    _locationPermission.onRestrictedCallback(() {
      setState(() => locationState = PermissionStatus.restricted);
    });

    _notificationPermission.onDeniedCallback(() {
      setState(() => notificationState = PermissionStatus.denied);
    });
    _notificationPermission.onGrantedCallback(() {
      setState(() => notificationState = PermissionStatus.granted);
    });
    _notificationPermission.onLimitedCallback(() {
      setState(() => notificationState = PermissionStatus.limited);
    });
    _notificationPermission.onPermanentlyDeniedCallback(() {
      setState(() => notificationState = PermissionStatus.permanentlyDenied);
    });
    _notificationPermission.onProvisionalCallback(() {
      setState(() => notificationState = PermissionStatus.provisional);
    });
    _notificationPermission.onRestrictedCallback(() {
      setState(() => notificationState = PermissionStatus.restricted);
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SafeArea(
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                for (final permissionEntry
                    in {
                      _locationPermission: locationState,
                      _notificationPermission: notificationState,
                    }.entries)
                  Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text("${permissionEntry.key}: ${permissionEntry.value}"),
                      TextButton(
                        onPressed: () {
                          permissionEntry.key.request();
                        },
                        child: Text("REQUEST ${permissionEntry.key}"),
                      ),
                    ],
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Let me know if I'm doing anything wrong here (and if I am, why it's wrong and how to do it right).

Also, you mention the example app, but the example app in permission_handler/example doesn't feature any permission callbacks. To be clear, this error only happens when you have permission callbacks for two or more permissions.

12people avatar Mar 18 '25 10:03 12people

Here's a recording of the new code:

https://github.com/user-attachments/assets/c46f5544-43ae-4711-a548-d8d0258f2777

Notice how the notification-related callbacks are run when the location permission is set, rather than the location-related callbacks that should be run.

12people avatar Mar 18 '25 10:03 12people

@TimHoogstrate Any updates on this? Anything else I should provide?

12people avatar Jun 02 '25 05:06 12people

@12people,

Can you verify this with the Android example app? I cannot reproduce this in the example app, so it should be something in your code (or any other factor).

Kind regards,

TimHoogstrate avatar Jun 04 '25 10:06 TimHoogstrate

@TimHoogstrate I addressed this in a previous comment:

Also, you mention the example app, but the example app in permission_handler/example doesn't feature any permission callbacks. To be clear, this error only happens when you have permission callbacks for two or more permissions.

So I'm not sure how I could reproduce this there other than by adding the same kind of code as in my example above.

12people avatar Jun 04 '25 11:06 12people

Hi everyone,

I'm facing the same issue described in this thread, and I wanted to provide more details regarding what seems to be happening.

When registering multiple onXXXCallback functions, only the last one registered gets executed, regardless of which permission is actually granted.

For example:

_locationPermission.onGrantedCallback(() {
  setState(() => locationState = PermissionStatus.granted);
});

_notificationPermission.onGrantedCallback(() {
  setState(() => notificationState = PermissionStatus.granted);
});

In this case, only the callback for _notificationPermission is called, even if the user grants location permission. This makes it impossible to handle multiple permissions with individual callbacks properly, as earlier registered callbacks are essentially ignored or overwritten.

It looks like the onGrantedCallback is not scoped per permission instance as expected, but rather globally shared or overwritten internally.

Would appreciate any updates or potential workarounds from the maintainers or community.

Thanks!

romainpurchla avatar Jul 22 '25 13:07 romainpurchla

Hi @romainpurchla

Not sure but perhaps doing like the documentation :

// You can request multiple permissions at once.
Map<Permission, PermissionStatus> statuses = await [
  Permission.location,
  Permission.storage,
].request();

print(statuses[Permission.location]);

Cyrille37 avatar Aug 07 '25 13:08 Cyrille37

@Cyrille37 That solves asking for multiple permissions, but not watching the changing state of those permissions via callbacks.

My workaround so far has been to have a family notifier in Riverpod to keep track of the permission state, and always requesting permissions through it. (However, unfortunately, it doesn't track the state if the user changes the permissions outside of the app UI -- e.g. via system app settings -- while the app is running.)

12people avatar Aug 07 '25 14:08 12people