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

Android 13 Permission.notification returns denied

Open otopba opened this issue 2 years ago • 2 comments

🐛 Bug Report

If the user cancels notification permission request several times then the next time we get a denied value when check permissionStatus.

Although, we can no longer ask for permission. And we need it to be permanentlyDenied

Otherwise, it turns out that the interface has a button "allow access to notifications" but when you click on it nothing happens

Expected behavior

If the user has canceled the access request several times, I want to get the status permanentlyDenied

Reproduction steps

Take Android 13 and reject the notification access request several times

Configuration

Version: 10.0.0

Platform:

  • [ ] :iphone: iOS
  • [x] :robot: Android

otopba avatar Sep 01 '22 15:09 otopba

Otherwise, it turns out that the interface has a button "allow access to notifications" but when you click on it nothing happens

I encountered an issue that sounds similar, unrelated to this plugin, which I think the Android 13 docs communicate poorly.

Using

  • targetSdkVersion 33
  • without <uses-permission ... POST_NOTIFICATIONS" /> in AndroidManifest.xml,

then a

  • new app install
  • on an Android 13 device

has its system settings toggle (App Info -> Permissions -> Notifications) disabled. (edit: Not just off. Disabled.)

A workaround is to roll back targetSdk to 32, and (/or?) add the manifest permission.

mockturtl avatar Sep 02 '22 18:09 mockturtl

A temp workaround is to downgrade your targetSdkVersion to 32, while keeping compileSdkVersion on 33 to cater for firebase requirements.

Theunodb avatar Sep 14 '22 05:09 Theunodb

Otherwise, it turns out that the interface has a button "allow access to notifications" but when you click on it nothing happens

I encountered an issue that sounds similar, unrelated to this plugin, which I think the Android 13 docs communicate poorly.

Using

  • targetSdkVersion 33
  • without <uses-permission ... POST_NOTIFICATIONS" /> in AndroidManifest.xml,

then a

  • new app install
  • on an Android 13 device

has its system settings toggle (App Info -> Permissions -> Notifications) disabled.

A workaround is to roll back targetSdk to 32, and (/or?) add the manifest permission.

Android 13 has the notifications permissions switched off by default as described here https://developer.android.com/develop/ui/views/notifications/notification-permission#new-apps.

But to specify this issue a little bit more:

When using await Permission.notification.request() the correct behavior can be observed for android 13. After denying the permission for a second time the request dialogue won't show up anymore and the status permanentlyDenied is true will be returned.

But if I try to just get the status via Permission.notification.status.isPermanentlyDenied or Permission.notification.isPermanentlyDenied right after I denied the Dialogue for a second time isPermanentlyDenied will be false, expected behavior ist that isPermanentlyDenied is true.

brim-borium avatar Sep 22 '22 13:09 brim-borium

I am facing the same issue. The notification request pops up when the app is in the background and a notification is delivered.

meet7-sagar23 avatar Oct 01 '22 14:10 meet7-sagar23

I'm also having the same problem with Permission.storage, after denying a second time I always get isDenied true and isPermanentlyDenied false

sebaslogen avatar Oct 04 '22 15:10 sebaslogen

I have the same behavior on iOS when I check the notification permission. Always returns denied even though I granted

kkoken avatar Oct 13 '22 09:10 kkoken

Has anyone found a fix for this? I'm experiencing the same issue. The notification request dialogue isn't showing up, but the logic to handle that is thrown off by the fact that the status is denied rather than permanentlyDenied.

jlandiseigsti avatar Nov 28 '22 21:11 jlandiseigsti

Faced with same issue:(

sanekyy avatar Jan 30 '23 10:01 sanekyy

Hello! I think that I found bug in this approach.

If I will not click on buttons in system dialog and just close dialog via click outside of dialog, on third iteration permission.request(); will show system dialog and then before and after will be both false, as a result, we will end up in a block with print('No more permission pop-ups displayed');

Do anyone have any idea what we can do with that?

sanekyy avatar Feb 08 '23 12:02 sanekyy

@sanekyy When you reached that specific situation, you can just open the app settings programmatically. e.g. using the function openAppSettings of the pub.dev package app_settings

h3x4d3c1m4l avatar Feb 09 '23 18:02 h3x4d3c1m4l

Problem is that in that case user see system dialog first, skip then and there we have to end flow.

But we open settings:(

sanekyy avatar Feb 10 '23 09:02 sanekyy

Same issue

I don't want to show a message to the user if he make a decision to disable notifications permanently. My temporary solution is:

  1. Save a flag in preferences as soon as Permission.notification.request() returns isPermanentlyDenied status.
  2. Call openAppSettings() when previous status is denied and new status (after call request() ) is isPermanentlyDenied, and when it was not a long time between calling Permission.notification.status and Permission.notification.request()

So, when user click the button 'Allow access', he will see notifications dialog or app settings. And then, I don't show the message and the button if Permission.notification.status is disabled and the flag from permissions is true.

Main disadvantages are:

  • If a user clicks "deny" very fast, openSettings() will be call just after dialog complete
  • If request() is slow when it is permanently denied, it returns "denied" status
  • app settings will be opened instead of usual dialog on the third interaction

It looks like this:

///Temporary fix https://github.com/Baseflow/flutter-permission-handler/issues/902
extension TemporaryNotificationsStatusSolution on Permission {
  static const _permissionName = "notificationsPermanentlyDenied";
  static const _diffInMills = 500;

  Future<PermissionStatus> notificationStatus() async {
    var permissionStatus = await Permission.notification.status;
    if (!permissionStatus.isDenied || !(await _isAndroidSdk33())) {
      return permissionStatus;
    }

    final preferences = await SharedPreferences.getInstance();
    final flag = preferences.getBool(_permissionName);
    if (flag == true && permissionStatus.isDenied) {
      return PermissionStatus.permanentlyDenied;
    }

    return permissionStatus;
  }

  ///returns null when it opens App Settings
  Future<PermissionStatus?> notificationRequest() async {
    if (!(await _isAndroidSdk33())) {
      return await Permission.notification.request();
    }

    final checkStatusTime = DateTime.now();
    final status = await Permission.notification.status;
    final newStatus = await Permission.notification.request();
    final newCheckStatusTime = DateTime.now();
    Duration difference = newCheckStatusTime.difference(checkStatusTime);
    logger.d(difference.inMilliseconds);

    if (status.isDenied &&
        newStatus.isPermanentlyDenied &&
        difference.inMilliseconds < _diffInMills) {
      final preferences = await SharedPreferences.getInstance();
      preferences.setBool(_permissionName, true);
      openAppSettings();
      return null;
    }

    return newStatus;
  }

  Future<bool> _isAndroidSdk33() async {
    if (defaultTargetPlatform != TargetPlatform.android) {
      return false;
    }
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
    return androidInfo.version.sdkInt == 33;
  }
}

And then I call Permission.notification.notificationStatus(); instead of Permission.notification.status and Permission.notification.notificationRequest(); instead of Permission.notification.request()

For example:

class _NotificationPermissionsExampleState
    extends State<NotificationPermissionsExample> with WidgetsBindingObserver {
  bool _needToShow = false;

  @override
  Widget build(BuildContext context) {
    if (!_needToShow) {
      return const Text(
          "Permissions are granted, limited or permanently denied");
    }

    return Column(
      children: [
        const Text("Please grant location permissions"),
        OutlinedButton(
            onPressed: _requestPermissions,
            child: const Text("Grant permissions"))
      ],
    );
  }

  @override
  void initState() {
    _checkForPermissions();
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      _checkForPermissions();
    }
  }

  Future<void> _checkForPermissions(
      {PermissionStatus? permissionStatus}) async {
    final status =
        permissionStatus ?? await Permission.notification.notificationStatus();
    setState(() {
      _needToShow =
          !status.isGranted && !status.isLimited && !status.isPermanentlyDenied;
    });
  }

  Future<void> _requestPermissions() async {
    final permissionStatus =
        await Permission.notification.notificationRequest();
    if (permissionStatus != null) {
      _checkForPermissions(permissionStatus: permissionStatus);
    }
  }
}

KlGleb avatar Feb 23 '23 10:02 KlGleb

This might help https://www.youtube.com/watch?v=uMvGpBOT0ZY

myselfuser1 avatar Mar 17 '23 12:03 myselfuser1

I am having other issue with respect to getting the status of the notification permission.

When the app installed first time, When i check for
final notificationStatus = await Permission.notification.status;

Excepted behaviour : Status should be UNKNOWN or not determined

Actual behaviour : Status always Denied.

I need the status as unknown in app first lunch to show some explanations before i show the OS permission pop-up.

Avinash01111992 avatar May 04 '23 12:05 Avinash01111992

@Avinash01111992 Hi!

Usually this task solves via bool pref in preferences manager.

sanekyy avatar May 04 '23 13:05 sanekyy

@sanekyy Hello

Thank you for you'er reply.

What you mean by via bool pref in preferences manager

Can you eleborate more on this solution.

Do I need to store the permission status in the front end ?? How ? Because when the app starts up , Status should be returned as unknown that is not coming , and also I should know when permission permanently disabled.

AvinashGowda-11 avatar May 08 '23 03:05 AvinashGowda-11

@AvinashGowda-11, here's how I am handling it. Let me know if this works for you as well.

class _PermissionUtilities {
  Future<bool> requestCameraPermission() async {
    PermissionStatus result = await Permission.camera.request();

    if (result.isGranted) {
      return true;
    } else if (Platform.isIOS || result.isPermanentlyDenied) {
      HapticFeedback.mediumImpact();
      return false;
    } else {
      return false;
    }
  }

  Future<bool> requestMicrophonePermission() async {
    PermissionStatus result = await Permission.microphone.request();

    if (result.isGranted) {
      return true;
    } else if (Platform.isIOS || result.isPermanentlyDenied) {
      HapticFeedback.mediumImpact();
      return false;
    } else {
      return false;
    }
  }

  Future<void> openAppSettings() {
    return AppSettings.openAppSettings(
      asAnotherTask: true,
    );
  }

  Future<bool> requestLocationPermission() async {
    Map<Permission, PermissionStatus> _statuses = await [
      Permission.location,
      Permission.locationAlways,
      Permission.locationWhenInUse,
    ].request();
    bool _isGranted = false;
    _statuses.forEach(
      (key, value) async {
        final bool _granted = value.isGranted;
        if (_granted) {
          _isGranted = _granted;
        }
      },
    );

    return _isGranted;
  }

  Future<bool> requestPhotosPermission() async {
    PermissionStatus result = PermissionStatus.granted;
    // In Android we need to request the storage permission,
    // while in iOS is the photos permission
    if (Platform.isIOS) {
      // result = await Permission.storage.request();
      result = await Permission.photos.request();
    }
    if (result.isGranted || result.isLimited) {
      return true;
    } else if (Platform.isIOS || result.isPermanentlyDenied) {
      HapticFeedback.mediumImpact();
      return false;
    } else {
      return false;
    }
  }
}

// How I am using it: -
class Utilities {
  static _PermissionUtilities permission = _PermissionUtilities();
}

// And how I access the functions: -
final bool _isApproved = await Utilities.permission.requestCameraPermission();

So, what I have done in the above code is that I created a static singleton class Utilities and then accessing the permission request functions from the _PermissionUtilities class. You can also understand the functionality from the function's code. In that class I am checking the permission on the basis of platform. I hope this works for you as well.

meet7-sagar23 avatar May 08 '23 05:05 meet7-sagar23

This is a common issue and unfortunately nothing we can resolve.

Androids native method to check permission status will on return granted or denied and provides not further information (e.g. if permissions are permanently denied). This is also explained on our Wiki page.

The recommended way to handle this is to check permissions and if the result is PermissionStatus.denied go ahead and request permissions. When permissions are permanently denied, Android will return immediately with the PermisisonStatus.permanentlyDenied status and not show any dialog to the user.

mvanbeusekom avatar Aug 10 '23 13:08 mvanbeusekom

This is a common issue and unfortunately nothing we can resolve.

Androids native method to check permission status will on return granted or denied and provides not further information (e.g. if permissions are permanently denied). This is also explained on our Wiki page.

The recommended way to handle this is to check permissions and if the result is PermissionStatus.denied go ahead and request permissions. When permissions are permanently denied, Android will return immediately with the PermisisonStatus.permanentlyDenied status and not show any dialog to the user.

but on 13 and higher, permanentlyDenied only return by request() prompt if user has denied the very first one. if first time user chose allow, request() can never return permanentlyDenied again. even manually change permission to deny and deny again from request() prompt in app.

ericccg avatar Sep 20 '23 07:09 ericccg