Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

checkNotifications returns "denied" in all cases on Android 13 #714

Closed
lightrow opened this issue Sep 6, 2022 · 18 comments
Closed

checkNotifications returns "denied" in all cases on Android 13 #714

lightrow opened this issue Sep 6, 2022 · 18 comments
Assignees
Labels
bug Something isn't working

Comments

@lightrow
Copy link

lightrow commented Sep 6, 2022

Bug summary

When checking and requesting notifications permission on Android 13, there is now no way to tell whether you should try requesting notifications permission or take user to settings screen, unless you manually store the amount of failed attempts to request permission, because the status is always "denied" no matter if it's first launch or if user clicked "Don't allow" twice or disabled it manually after allowing it.

Library version

3.6.1

Environment info

[16:49] ❯ npx react-native info
info Fetching system and libraries information...
System:
    OS: macOS 12.4
    CPU: (10) arm64 Apple M1 Pro
    Memory: 115.19 MB / 32.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.13.1 - /usr/local/bin/node
    Yarn: 1.22.18 - ~/.nvm/versions/node/v16.14.2/bin/yarn
    npm: 8.5.0 - ~/.nvm/versions/node/v16.14.2/bin/npm
    Watchman: 2022.08.22.00 - /opt/homebrew/bin/watchman
  Managers:
    CocoaPods: 1.11.3 - /Users/groza/.rbenv/shims/pod
  SDKs:
    iOS SDK:
      Platforms: DriverKit 21.4, iOS 15.5, macOS 12.3, tvOS 15.4, watchOS 8.5
    Android SDK:
      Android NDK: 22.1.7171670
  IDEs:
    Android Studio: 2021.2 AI-212.5712.43.2112.8815526
    Xcode: 13.4.1/13F100 - /usr/bin/xcodebuild
  Languages:
    Java: 11.0.14 - /usr/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 17.0.2 => 17.0.2 
    react-native: 0.68.0 => 0.68.0 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found

Steps to reproduce

  1. Launching the app on Android 13 emulator for the first time and checking permission - checkNotifications returns "denied"
  2. Asking for permission with requestNotifications, system prompt is shown and user denies it - the returned status remains "denied"
  3. Asking for permission again, system prompt is shown again and user denies it again - "denied"
  4. Asking for permission one more time and system prompt is not shown anymore, the status is still "denied"

Reproducible sample code

import { checkNotifications, requestNotifications } from 'react-native-permissions';

const requestPermission = async () => {
    await requestNotifications(['alert',  'sound']);
    const status = await checkNotifications();
    console.log(status);
}

i did set targetSdkVersion to 33

 ext {
        buildToolsVersion = "33.0.0"
        minSdkVersion = 23
        compileSdkVersion = 33
        targetSdkVersion = 33
        ndkVersion = "22.1.7171670"
        supportLibVersion = "33.0.0"
    }
@lightrow lightrow added the bug Something isn't working label Sep 6, 2022
@zoontek zoontek closed this as completed Sep 6, 2022
@zoontek
Copy link
Owner

zoontek commented Sep 6, 2022

Hi. This is not a bug, check the Android permission flow.
Without relying on hacks (like app storage - which break other features like "Ask next time"), we cannot know if a permission is blocked without requesting it.

Consider this flow:

  • before showing the screen that require the permission, check it
  • if it's granted, mount the screen that use it
  • if it blocked, display a screen that explain that the user will have to go to settings to enable it
  • if it's denied, display a screen with a button to perform a request. after that, display an alert / the blocked screen before encouraging going to the settings

It works on all OS and have a good enough UX.

@lightrow
Copy link
Author

lightrow commented Sep 14, 2022

@zoontek but that's the point, what you are suggesting is now impossible on Android 13.

on iOS and Android prior to 13 checkNotifications() method returns "granted", "denied" or "blocked". But on Android 13 it only returns either "granted" or "denied" - "blocked" is never returned even when it's actually blocked.

const { status: checkstatus } = await checkNotifications();
console.log('check permission result:', checkStatus);
const { status } = await requestNotifications(['alert', 'sound']);
console.log('request permission result:', status);

when notification access is already blocked, on Android 13 this returns

 LOG  check permission result: denied
 LOG  request permission result: blocked

and on Android 12 and iOS this returns

 LOG  check permission result: blocked // notice the check is now correct
 LOG  request permission result: blocked

@nicwise
Copy link

nicwise commented Oct 10, 2022

Did you find a solution to this? I suspect it might be related to this, but I can't make it work even with adding the permission

https://developer.android.com/develop/ui/views/notifications/notification-permission

@akshgods
Copy link

any help on this?

@zoontek
Copy link
Owner

zoontek commented Dec 6, 2023

Reading the first line of the Android documentation @nicwise posted:

Android 13 (API level 33) and higher supports a runtime permission

  • On Android < 13, both checkNotifications and requestNotifications just check areNotificationsEnabled(), return granted or blocked (which is normal)
  • On Android >= 13, it works exactly like the other runtime permissions, follow the same Android flow and rules that the ones in the README.

There's no issue here.

EDIT: More infos about the runtime flow, which is the same for all Android runtime permissions: #828 (comment)

@chichilatte
Copy link

Hi @zoontek, sorry to keep asking about this. I see there are at least 3 duplicate issues about it! Are we being really stupid?

Here is our problem, using your flow for Android >=13 ...

  • before showing the screen that require the permission, check it

FINE. We'll use checkNotifications() to do that.


  • if it's granted, mount the screen that use it

FINE. checkNotifications() can return "granted" :)


  • if it blocked, display a screen that explain that the user will have to go to settings to enable it

NOT FINE. checkNotifications() never returns "blocked" on Android >= 13


  • if it's denied, display a screen with a button to perform a request. after that, display an alert / the blocked screen before encouraging going to the settings

NOT FINE. checkNotifications() returns "denied" if the user has previously blocked the permission.
So our user will always see the "screen with the button to perform a request".

Thanks for your patience :)

@zoontek
Copy link
Owner

zoontek commented Dec 14, 2023

@chichilatte OK, let's start again from zero.

  • On iOS, check can return blocked. All permissions are runtime permissions.
  • On Android, runtime permissions check (the ones that prompt the user with an alert on request) cannot return blocked
  • On Android < 13, checkNotifications isn't a runtime permission (no prompt on request). The check only perform a simple condition: NotificationManagerCompat.from(getReactApplicationContext()).areNotificationsEnabled() and resolves with granted or blocked (as this is not a runtime permission, but more a "is the feature enabled?" we can return blocked)
  • Since Android 13, notifications is a runtime permission and must be requested. Which means it cannot be blocked on check

This is the runtime permission flow on Android:

Screenshot 2023-12-14 at 10 50 38

So, let's recap:

  • before showing the screen that require the permission, check it

FINE. We'll use checkNotifications() to do that.

  • if it's granted, mount the screen that use it

FINE. checkNotifications() can return "granted" :)

All good.

  • if it blocked, display a screen that explain that the user will have to go to settings to enable it

NOT FINE. checkNotifications() never returns "blocked" on Android >= 13

Yes, but iOS and Android < 13 could. You can improve their UX over Android >= 13 and its not-so-good runtime permission API (which makes this impossible).

  • if it's denied, display a screen with a button to perform a request. after that, display an alert / the blocked screen before encouraging going to the settings

NOT FINE. checkNotifications() returns "denied" if the user has previously blocked the permission.
So our user will always see the "screen with the button to perform a request".

Exactly like all the others runtime permissions on Android, since Android 6. Have you tried check / request on PERMISSIONS.ANDROID.CAMERA, for example? 😄 It follows the Android flow described in the README, no blocked before request.

The checkNotifications behaviour on Android < 13 was an exception and was not following the check flow because it was not a runtime permission and it was not requestable. Since Android 13, the permission must be requested and checkNotifications / requestNotifications behave like all the other runtime permissions, which means they follow the flow and this warning:

readme

Even simpler, if you check the checkNotifications and requestNotifications source code, you will see it behaves differently on Android < 13: checkNotifications does not call check, requestNotifications even perform a checkNotifications as it could not be requested before Android 13

But if no one seems to understand it, I will simply return denied on Android < 13 (even if I could return blocked in such cases), at least the UX will be identical to Android >= 13, but not identical to the iOS one (which is better, since you avoid the screen with the button to request) 🤷🏻‍♂️

When I give this kind of scenario, keep in mind that it's a solution to easily handle all permissions, on all platforms given their respectives limitations.

  • In the notifications case, iOS and Android < 13 will have a better UX (a screen that says it's blocked, not the alert that says it once you pressed the request button)
  • In the camera case, all versions of Android will have a poorer UX than iOS (no screen that says it's blocked, only the alert that says it once you pressed the request button) as this is, like notifications since Android 13 and all the others, a runtime permission.

I hope this is clear.

@zoontek
Copy link
Owner

zoontek commented Dec 14, 2023

Here, it will be easier for everyone: https://github.com/zoontek/react-native-permissions/releases/tag/4.0.1

checkNotifications now behaves the same on Android < 13 and Android >= 13, it now cannot resolves with blocked and you have to request it to get the info. Before, your users might benefits from a slightly better UX (the one you prepared for iOS, with a blocked screen and more infos) on those versions of Android, but I get that it introduces confusion if you are only targeting Android.

Also, it makes less sense since you might see a request permission button on Android < 13, but it will never be able to prompt you, you will have to redirect to settings to update the value.

@chichilatte
Copy link

Wow, thanks for replying so thoroughly @zoontek, truly appreciated.

So here's what i understand from all you've said in this thread, and from the Android flow diagram...

On Android >= 13:

  • checkNotifications()never returns "blocked". Even if the user has previously blocked.

  • You must call requestPermission() to find out if a permission is blocked.


From these two points you can conclude:

On Android >=13 we cannot show the user a message explaining why they should grant permission (aka the rationale) before we open the runtime system permission dialog.

Which is weird, because the Android docs recommend we do show a rationale.
(I see that Android does have a shouldShowRequestPermissionRationale() method, perhaps that might help.)

My use case is asking for notifications permission only once, at a specific time after install. That's not a use case Android envisaged, so the best solution for me is probably...

  • Show rationale
  • Store that we requested permission
  • Never show rationale or request permission again

That's as clear as I can be. If I'm still mistaken about anything then i must have gone mad, my apologies 🤣 🔫

@chichilatte
Copy link

chichilatte commented Dec 14, 2023

Here, it will be easier for everyone: https://github.com/zoontek/react-native-permissions/releases/tag/4.0.1

checkNotifications now behaves the same on Android < 13 and Android >= 13, it now cannot resolves with blocked and you have to request it to get the info. Before, your users might benefits from a slightly better UX (the one you prepared for iOS, with a blocked screen and more infos) on those versions of Android, but I get that it introduces confusion if you are only targeting Android.

Also, it makes less sense since you might see a request permission button on Android < 13, but it will never be able to prompt you, you will have to redirect to settings to update the value.

Personally it like that Android < 13 behaves differently (and more rationally). And it would be a breaking change. A difficult change to get your head around.

I'm more confused by how Android >= 13 is not able to say if permission has been blocked before we request permission.

@zoontek
Copy link
Owner

zoontek commented Dec 14, 2023

So here's what i understand from all you've said in this thread, and from the Android flow diagram...

On Android >= 13:

  • checkNotifications()never returns "blocked". Even if the user has previously blocked.
  • You must call requestPermission() to find out if a permission is blocked.

Exactly, and now with 4.0.1 it's the same on Android < 13.


On Android >=13 we cannot show the user a message explaining why they should grant permission (aka the rationale) before we open the runtime system permission dialog.

Which is weird, because the Android docs recommend we do show a rationale.
(I see that Android does have a shouldShowRequestPermissionRationale() method, perhaps that might help.)

What it says is actually this:

If the ContextCompat.checkSelfPermission() method returns PERMISSION_DENIED, call shouldShowRequestPermissionRationale(). If this method returns true, show an educational UI to the user. In this UI, describe why the feature that the user wants to enable needs a particular permission.


So, now, let me explain how shouldShowRequestPermissionRationale works with runtime permissions 🫡

Before Android 11, you could request permissions as much as you want:

  • On first request call, you have Deny or Grant
  • If the user deny, it will result in denied, as it's still requestable
  • On second request call, you also have these 2 choices, but also have a checkbox "Never ask again"
  • If the user tick the checkbox, calling request will just result in blocked
  • If he doesn't, it will stay denied as you can call request as much as you want until he ticks the checkbox

Since Android 11, this is a bit different:

  • On first request call, you have Deny or Grant
  • If the user deny, it will result in denied, as it's still requestable
  • On second request call, you also have these 2 choices, but don't have the checkbox "Never ask again" anymore
  • If the user deny, it will result in blocked, as it's not requestable anymore

Keep that in mind. Now, let's investigate on shouldShowRequestPermissionRationale (which has request in his name and is mentioned on the requesting page in the android doc, it's a clue 🕵️)

This function is called before the native request if you set a rationale parameter (source code here)

  • On first request call, it will always resolves to false
  • When the permission request has been denied once, the permission is still requestable.
  • You call request a second time (with a rationale), it will resolves to true and you will be able to beg the user not to tick the "Never ask again" / press "Deny" a second time (you guessed it, this step can be repeated as much as you want on Android < 11 until the user ticks the checkbox)
  • When the user ticked the checkbox / denied for a second time, request results in blocked. Calling shouldShowRequestPermissionRationale after this would resolves with false (as it's not requestable anyway!)

So, how to determine if a permission is blocked (requestable) before actually requesting it? If check resolves with denied and shouldShowRequestPermissionRationale with false, does this means that:

  • It's the first time you call request
  • The permission is not requestable anymore

You can't know 🤷🏻‍♂️

That's also the reason the builtin PermissionAndroid module check returns a boolean and not a PermissionStatus, like the result function does. This library has the same limitation, it just return a status because we have the unavailable extra status / for parity with other platforms like iOS, which can returns blocked on check.

And before you ask me, I know that at some time some developers saved already requested (and blocked) permissions in SharedPreferences (this library also did that before Android 11 and you are about to do the same mistake). It allowed us to know if a permission is blocked on check, but since the "Ask next time" / "Only this time" permissions settings appeared, we cannot rely on this trick anymore.


Personally it like that Android < 13 behaves differently (and more rationally). And it would be a breaking change.

Actually no, the returned type always has been PermissionStatus and you had to handle the denied case for Android > 13.

I'm more confused by how Android >= 13 is not able to say if permission has been blocked before we request permission.

This is how every runtime permissions on Android works. I deeply encourage you to try checking / requesting other ones (you have the example app for that).

A difficult change to get your head around.

As it now aligns to the android permission flow / all the others permissions check, I think it will be easier for everyone 🙂

@chichilatte
Copy link

chichilatte commented Dec 14, 2023

You can't know 🤷🏻‍♂️

Ahhh.

So, correct me if I'm wrong, on Android >= 13 we can have this fairly common flow...

  • The app hits a situation where it's time to ask for a permission
  • The app displays a rationale
  • The user then sees the system's permission request prompt
  • They tap "Don't allow"

The next time the situation comes the user will go through the same flow again. Even though they said "Don't Allow" the first time. Since Android 11 this can happen only twice. Further repetitions of the flow will look like this...

  • The app hits a situation where it's time to ask for a permission
  • The app displays a rationale (we show it because we cannot know if permission is blocked)
  • The system permission prompt does not show (since the user has blocked permission twice)

As far as i can see, if you want the flow to happen only once then you must store the fact that the user blocked permission. And in doing that you may be ignoring the user's wish to Ask next time. It's a compromise.

I have to say @zoontek you're doing an incredible job with this thicket of detail. It seems to me the Android permission system is so complex it's basically broken!

@zoontek
Copy link
Owner

zoontek commented Dec 14, 2023

It's a compromise indeed. I chose to go for the poorer UX and support "Ask next time" as storing the value is still possible on your side, you accept to break it.

And yes, Android permission system is broken 🥲

@chichilatte
Copy link

chichilatte commented Dec 15, 2023

Just to be clear for anybody who sees this thread...

On Android >=13 you cannot display a rationale message before opening the system's permission request prompt.

This is because Android does not let you know if permission is blocked unless you request permission.
Android does however let you know if permission is denied (the default) or granted.

There is a workaround where you store the status returned by requestPermission(). Subsequently you can prevent the rationale from displaying again if the stored status is blocked.

(happy to edit this if needs correcting)


Edit

You can pass a rationale argument in the requestPermission() call:

  • Request permission with rationale argument
  • System permission prompt opens (no rationale will be displayed)
  • User taps "Don't allow"
  • Later on we request permission again, with rationale argument
  • Rationale is displayed
  • If rationale resolves to true then the system permission prompt will open
  • Further calls to request the same permission will be ignored

@zoontek
Copy link
Owner

zoontek commented Dec 15, 2023

On Android >=13 you cannot display a rationale message before opening the system's permission request prompt.

You can, it should appear before the second request if you pass a rationale argument (when calling shouldShowRequestPermissionRationale it will be true at this exact time) 😄. The rest is OK.

@chichilatte
Copy link

chichilatte commented Dec 15, 2023

Oh i didn't even see the rationale parameter in your docs @zoontek. But showing the rationale only before the second permission request, after the permission has already been blocked once, wtf were the Android people thinking? 😆 🤦🏻

@zoontek
Copy link
Owner

zoontek commented Dec 15, 2023

I think they though "Maybe if we allow devs to know that the user has refused once, and they can beg the user before re-doing it, it will be a nice experience" 😅

@anhquan291
Copy link

Thank you guys so much. It's super clear :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

6 participants