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

Support permission access on Android 11 #422

Closed
biendinh opened this issue Nov 9, 2020 · 18 comments
Closed

Support permission access on Android 11 #422

biendinh opened this issue Nov 9, 2020 · 18 comments
Labels
android Relates to Android platform caveat A caveat of using this plugin investigate Issue requires investigation runtime issue An issue related to app runtime

Comments

@biendinh
Copy link

biendinh commented Nov 9, 2020

  • I just test with getCameraAuthorizationStatus()
  • When user manual select "Ask very time" in Setting Permission, plugin always return "DENIED_ALWAYS". It maybe wrong behavior, any suggestion?

Screenshot_20201109-190943

@dpa99c
Copy link
Owner

dpa99c commented Nov 9, 2020

Check you have added the required permissions for getCameraAuthorizationStatus() to AndroidManifest.xml as outlined in the Android permissions documentation.

Also please build and run the example project app as a known working reference to rule out possible causes in your own codebase.

@dpa99c dpa99c added android Relates to Android platform awaiting response If no response, issue will be closed runtime issue An issue related to app runtime labels Nov 9, 2020
@biendinh
Copy link
Author

biendinh commented Nov 9, 2020

@dpa99c Thank for response.

My AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.FLASHLIGHT" /> <uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.USE_CREDENTIALS" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
It worked perfect if user allowed this permission or denied once, but appeared wrong behavior if user select "Ask every time" manually in Camera Access Setting (only Android 11, see my attachment as above).
I debugged and it return "DENIED_ALWAYS" when call getCameraAuthorizationStatus().

Please help.

@dpa99c dpa99c added caveat A caveat of using this plugin investigate Issue requires investigation and removed awaiting response If no response, issue will be closed labels Nov 10, 2020
@dpa99c
Copy link
Owner

dpa99c commented Nov 10, 2020

Upon investigation, this is caused by breaking changes in Android 11 to permission states that I've only now just become aware of.

In the native Android environment, there are only 3 permission states:

  • PackageManager.PERMISSION_GRANTED => Diagnostic.STATUS_GRANTED
  • PackageManager.PERMISSION_DENIED
    • ActivityCompat.shouldShowRequestPermissionRationale() == false
      => Diagnostic.STATUS_DENIED_ONCE
    • ActivityCompat.shouldShowRequestPermissionRationale() == true
      => Diagnostic.STATUS_NOT_REQUESTED & Diagnostic.STATUS_DENIED_ALWAYS

In order to distinguish Diagnostic.STATUS_NOT_REQUESTED from Diagnostic.STATUS_DENIED_ALWAYS, this plugin stores a persistent flag when you request a permission for the first time (during the current app instance installation).
Therefore when PackageManager.PERMISSION_DENIED && ActivityCompat.shouldShowRequestPermissionRationale() == true, it uses this flag to determine if a permission has been requested before and therefore whether to return Diagnostic.STATUS_NOT_REQUESTED or Diagnostic.STATUS_DENIED_ALWAYS.

This works fine up until Android 10.

However Android 11, introduces some fundamental breaking changes to the permissions model as outlined here:

  • The "Don't ask again" checkbox has been removed which was previously shown when requesting a permission that the user had already denied once
    • Instead, if the user taps "Deny" again (twice), the permission is permanently denied and cannot be requested from the user again.
  • If your app requests a location/microphone/camera related permission, the new "Only this time" option is presented
    • if the user selects this, the permission will be granted for the current app session but will be revoked shortly after the current session ends.

Some empirical testing with the Camera permission on Android 11 shows the following results:

Before requesting permission:

  • Check permission => !granted && !showRationale && !isPermissionRequested => Diagnostic.STATUS_NOT_REQUESTED
  • Manually set "Only this time" in Settings => !granted && !showRationale && !isPermissionRequested => Diagnostic.STATUS_NOT_REQUESTED

Requesting permission:

  • Press "Deny" in request dialog - once => !granted && showRationale => Diagnostic.STATUS_DENIED_ONCE
  • Press "Deny" in request dialog (no "Don't ask again" checkbox) - twice => !granted && !showRationale && isPermissionRequested => Diagnostic.STATUS_DENIED_ALWAYS
  • Press "Only this time" in request dialog => granted => Diagnostic.STATUS_GRANTED
    => After app restart => !granted && !showRationale && isPermissionRequested => Diagnostic.STATUS_DENIED_ALWAYS
  • Press "While using the app" in request dialog => granted => Diagnostic.STATUS_GRANTED
    => After app restart => Diagnostic.STATUS_GRANTED
    => Manually set "Only this time" in Settings => !granted && !showRationale && isPermissionRequested => Diagnostic.STATUS_DENIED_ALWAYS

TL;DR: If the user selects the "Only this time" option (either via the permission dialog or directly via settings), once the permission has been revoked for the current app session, this plugin cannot distinguish between this state (where the permission can be requested again via the permissions dialog) and between the "don't ask again" state where the user has pressed "Deny" twice and the permission is permanently denied so cannot be requested again via the permissions dialog.

The only way your app can currently handle this outcome is if the reported status is Diagnostic.STATUS_DENIED_ALWAYS, then try requesting the permission again (e.g. with requestCameraAuthorization())

  • If the user had pressed "Only this time" in a previous app session, then calling requestCameraAuthorization() will show the permission dialog and you can handle the user's choice in the success callback.
  • If the user had previously pressed "Deny" twice, then calling requestCameraAuthorization() will NOT show the permission dialog and the success callback will be immediately invoked with Diagnostic.STATUS_DENIED_ALWAYS.

It may be possible for the plugin to do some heuristic interpretation in this scenario:

  • if the dialog is shown then it will take a certain amount of real time (several seconds?) for the user to make a choice and dimiss the dialog before the permission outcome handler function is invoked.
  • if the dialog is not shown, then the outcome handler will be invoked almost immediately.

So maybe the plugin can set a timer in this sceario to determine if the dialog has been shown and therefore whether to imply which of the above scenarios has taken place.
It will be some time before I'm able to try this out and come up with a workaround so in the meantime you are welcome to implement your own such workaround.

@biendinh
Copy link
Author

Excellent work, thank for your investigation.

@Adam4224
Copy link

Adam4224 commented Nov 23, 2020

How do you configure the rationale as shown to the user for a location request?

The reason I ask is that using "always" location authorisation is currently getting my app rejected - it seems that a customised rationale is required, but I can't see how to configure this.

@dpa99c
Copy link
Owner

dpa99c commented Nov 23, 2020

@Adam4224 Neither this plugin nor Android itself provide an out-of-the-box UI to display a rationale message to the user - it is up to you how to choose to display this, for example you could use cordova-plugin-dialogs to display an alert dialog containing the rational message.

In the case of requesting location "always" (i.e. background location permission), this has recently become much more complex due to changes Google has made to its Play Store policies - see here for details.
Specifically:

Your app must display a prominent disclosure through a pop-up alert before your app’s location runtime permission. Based on our review, a prominent disclosure did not appear before the runtime permission.

Please add a prominent disclosure and make sure it appears before the location runtime permission.

Remember, your prominent disclosure must:
Appear before your app’s location runtime permission.
Include at least the following sentence, adapted to include all the relevant features requesting access to location in the background in the app that are readily visible to the user: “This app collects location data to enable ["feature"], ["feature"], & ["feature"] even when the app is closed or not in use.”
Include any other details necessary to make it clear to the user how and why you are using location in the background. While additional content is permitted, it should not cause the required content to not be immediately visible.
Include the following sentence, if you extend permitted usage to ads: “This data is also used to provide ads.”

@Adam4224
Copy link

Adam4224 commented Nov 23, 2020 via email

@kunalpunjrath
Copy link

@dpa99c , the location permission statuses defined in the android module don't contain 'authorized_when_in_use' status, which is what is returned on an Android 11 device running an app with 'While Using The App' permission. Is there a fix you are planning to deal with thes new changes?

@CodeWithOz
Copy link

@biendinh @Adam4224 did you end up solving this problem with any workaround? If so, please can you share the strategy you used? Thanks.

@fcamblor
Copy link

fcamblor commented Jun 3, 2021

FYI, I had the exact same issue on Android 11 with cordova.plugins.diagnostic.requestLocationAuthorization() :

cordova.plugins.diagnostic.requestLocationAuthorization(console.info, console.error, cordova.plugins.diagnostic.locationAuthorizationMode.ALWAYS)

On Android 10 (and before) :

  • The call was opening the popup
  • It was asking a question to the user, and was providing the selected status

On Android 11 :

  • The call is not opening any popup (@dpa99c it looks like different than what you were saying in your detailed post)
  • It is always returning DENIED_ALWAYS status
  • Even when I am calling cordova.plugins.diagnostic.requestLocationAuthorization() multiple times, I'm getting DENIED_ALWAYS

@QuentinFarizon
Copy link

QuentinFarizon commented Aug 16, 2021

Hello,

Same as @fcamblor :

  • calling getLocationAuthorizationStatus return DENIED_ALWAYS (instead of NOT_REQUESTED on Android <= 10)
  • it always return DENIED_ALWAYS when calling requestLocationAuthorization(diagnostic.locationAuthorizationMode.ALWAYS) => I guess I should add a rationale popup before requesting, is that it ?
  • calling requestLocationAuthorization(diagnostic.locationAuthorizationMode.WHEN_IN_USE) prompts a system popup and returns GRANTED when granted

@biendinh
Copy link
Author

@biendinh @Adam4224 did you end up solving this problem with any workaround? If so, please can you share the strategy you used? Thanks.

My temporary solution:

  • check getCameraAuthorizationStatus return DENIED_ALWAYS, I call requestCameraAuthorization() again.

@expcapitaldev
Copy link

hi all!
@dpa99c Can I create merge request or are you going to make a fix in the next release?

@dpa99c
Copy link
Owner

dpa99c commented Jul 28, 2022

@dpa99c , the location permission statuses defined in the android module don't contain 'authorized_when_in_use' status, which is what is returned on an Android 11 device running an app with 'While Using The App' permission. Is there a fix you are planning to deal with thes new changes?

@kunalpunjrath Unlike iOS, which has a explicit constants to differentiate when-in-use vs always, Android does not provide such a differentiation.

In the case of location permission on Android 10 / API 29 and above, it's possible to infer this: if COARSE_LOCATION and/or FINE_LOCATION permission is granted, then whether BACKGROUND_LOCATION is also granted determines when-in-use vs always.
I am planning to release a new major version of this plugin (v7.0.0) which will incorporate this logic - ETA on this is 1-2 weeks from now.

However, for other permissions on Android where "When in use" is an option (e.g. Camera, Microphone), there's no way to differentiate between the user pressing "When in use" or "Allow once": in both cases, the outcome Android delivers to onRequestPermissionResult is PackageManager.PERMISSION_GRANTED

@dpa99c
Copy link
Owner

dpa99c commented Jul 28, 2022

FYI, I had the exact same issue on Android 11 with cordova.plugins.diagnostic.requestLocationAuthorization() :

cordova.plugins.diagnostic.requestLocationAuthorization(console.info, console.error, cordova.plugins.diagnostic.locationAuthorizationMode.ALWAYS)

On Android 10 (and before) :

  • The call was opening the popup
  • It was asking a question to the user, and was providing the selected status

On Android 11 :

  • The call is not opening any popup (@dpa99c it looks like different than what you were saying in your detailed post)
  • It is always returning DENIED_ALWAYS status
  • Even when I am calling cordova.plugins.diagnostic.requestLocationAuthorization() multiple times, I'm getting DENIED_ALWAYS

@fcamblor On Android 11+, you can no longer call requestLocationAuthorization() immediately with cordova.plugins.diagnostic.locationAuthorizationMode.ALWAYS - if you do, the call will not open a popup and the result will be DENIED_ALWAYS.

You must first call requestLocationAuthorization() with cordova.plugins.diagnostic.locationAuthorizationMode.WHEN_IN_USE then, if the user grants permission, call it again with cordova.plugins.diagnostic.locationAuthorizationMode.ALWAYS.

This is intentional behaviour of Android 11+, not an artefact of this plugin therefore it's not possible to change this behaviour.

@dpa99c
Copy link
Owner

dpa99c commented Jul 28, 2022

With regard to my earlier comment regarding a possible workaround for the behaviour when the user presses "Only this time":

It's not possible (as I suggested it might be in that comment) to workaround this issue by measuring the time taken between requesting a permission and receiving a permission request response.
This is because that approach cannot differentiate between permission being denied at the start of a new app session because in a previous session:

  • the user pressed "Don't allow" for a second time (permanently denying permission)
  • the user pressed "Only this time" (temporarily allowing permission and revoking in the next app seession)

In both cases, Android responds with PackageManager.PERMISSION_DENIED && !shouldShowRequestPermissionRationale so it's not possible to different if the permission is denied permanently (due to the first case) or temporarily (due to the second case).

Therefore the only way to handle this situation is: to check the current authorization status for the permission; if it returns DENIED_ALWAYS, assume this to be temporary denial (due to "Only this time") and then attempt to request permission again.
If the denial is indeed temporary, the user will be shown the permission choice dialog, and their choice will be passed back to the app as the request outcome.
If the denial is permanent, the user will not be shown the dialog, and the request outcome will be DENIED_ALWAYS.

If the outcome is that permission was denied, whether due to user actively denying permission, or implicitly due to previously denying permission twice (always), you would handle this in the same way (i.e. inform user that permission is needed and how to change in Settings).

Therefore I don't believe this plugin can be changed in order to work around behaviour which is intentional in the permissions model of Android 11+
and therefore I'm closing this issue.

@dpa99c dpa99c closed this as completed Jul 28, 2022
@dpa99c
Copy link
Owner

dpa99c commented Aug 5, 2022

As a reference for others, here's how I'm handling the "Only this time" permission on Android 11+ which results in permission status being returned as DENIED_ALWAYS in the next app session:
The solution is to assume that DENIED_ALWAYS means that permission can still be requested during this app session, until such time as it is actually requested.
Note: this assumption MAY be wrong and it may really be permanently denied but we won't know until we try requesting it!

You can see an working example of this in this example project: https://github.com/dpa99c/cordova-diagnostic-plugin-android-runtime-example/

Here's a short example using CAMERA permission.


let diagnostic, deviceOS;
let cameraDeniedAlwaysAfterRequesting = false;

function onDeviceReady(){
    diagnostic = cordova.plugins.diagnostic; // alias to shorter namespace
    diagnostic.getDeviceOSVersion(function(osDetails){
        deviceOS = osDetails;
        checkCameraPermission();
    })
}

function checkCameraPermission(){
    diagnostic.getPermissionAuthorizationStatus(function(status){

        // If running on Android 11+ and status is DENIED_ALWAYS, assume it can still be requested (i.e. user selected "Only once" in previous app session)
        if(deviceOS.apiLevel >= 30 && status === diagnostic.permissionStatus.DENIED_ALWAYS && !cameraDeniedAlwaysAfterRequesting){
            status = diagnostic.permissionStatus.DENIED_ONCE;
        }

        switch(status){
            case diagnostic.permissionStatus.GRANTED:
                console.log("Camera permission is allowed")
                break;
            case diagnostic.permissionStatus.NOT_REQUESTED:
                console.log("Camera permission not requested yet - requesting...")
                requestCameraPermission();
                break;
            case diagnostic.permissionStatus.DENIED_ONCE:
                console.log("Camera permission denied but can still request - requesting...")
                requestCameraPermission();
                break;
            case diagnostic.permissionStatus.DENIED_ONCE:
                console.log("Camera permission permanently denied - can't request");
                break;
        }
    }, console.error, diagnostic.permission.CAMERA)
};

function requestCameraPermission(){
    diagnostic.requestRuntimePermission(function(status){

        // If result is DENIED_ALWAYS after requesting then it really is permanently denied
        if(status === diagnostic.permissionStatus.DENIED_ALWAYS){
            cameraDeniedAlwaysAfterRequesting = true;
        }

        // Re-check permission
        checkCameraPermission();

    }, console.error, diagnostic.permission.CAMERA);
}

document.addEventListener("deviceready", onDeviceReady, false);

@peitschie
Copy link

Thanks @dpa99c ! I really appreciate you taking the time to document this. Stuff like this is never as simple as it seems, and having a good code snippet to follow makes a huge difference.

(thanks as an side for all the good plugins you maintain in the cordova ecosystem too)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
android Relates to Android platform caveat A caveat of using this plugin investigate Issue requires investigation runtime issue An issue related to app runtime
Projects
None yet
Development

No branches or pull requests

9 participants