Cordova runtime permissions - android

I'm trying to get the user to enable the location group permission for my app with Cordova's hasPermission/requestPermission methods, but the results are confusing...
When I call hasPermission with ACCESS_FINE_LOCATION, it always returns true. Calling this on Manifest.permission_group.LOCATION seems to return true/false appropriately.
Calling requestPermission with Manifest.permission_group.LOCATION doesn't present a system dialog, so I'm calling this with ACCESS_FINE_LOCATION to get the dialog.
The dialog Allow button turns on the Location group permission for my app and calls onRequestPermissionResult with PackageManager.PERMISSION_GRANTED, but the Deny button also returns ...GRANTED, leaving the Location group permission off.
For illustration, here's my current code:
private void checkPermissions() {
if (!cordova.hasPermission(Manifest.permission_group.LOCATION)) {
cordova.requestPermission(this, PERMISSION_RUNTIME_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION);
}
}
#Override
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
if (permissions.length != 1 || grantResults.length != 1 || !Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0])) {
throw new RuntimeException("Unexpected permission results " + Arrays.toString(permissions) + ", " + Arrays.toString(grantResults));
}
int result = grantResults[0];
String action = null;
switch (result) {
case PackageManager.PERMISSION_DENIED:
action = Constants.ACTION_RUNTIME_PERMISSION_DENIED;
break;
case PackageManager.PERMISSION_GRANTED:
action = Constants.ACTION_RUNTIME_PERMISSION_GRANTED;
break;
default:
throw new RuntimeException("Unexpected permission result int " + result);
}
Intent i = new Intent(action);
i.putExtra("permission", Constants.EXTRA_RUNTIME_PERMISSION_NOTIFICATION_ID);
getContext().sendBroadcast(i);
}
What's the right way to handle this? Sometimes using an individual permission and sometimes using a group with these methods doesn't seem right - I would expect this to be consistent. My guess is that the PERMISSION_GRANTED after the DENY button is pushed on the dialog is because I'm requesting an individual permission, which is on even though the group is off; is there a way to detect that the user denied the request?
I have a lot of questions there which basically boil down to "how do I either get the user to enable the Location group permission when it's off or know when they decline"?
If it helps, my android-targetSdkVersion is set to 22, and I'm using Cordova 6.1.1.

If it helps, my android-targetSdkVersion is set to 22, and I'm using Cordova 6.1.1.
Android run-time permissions were only introduced in API 23, so if your android-targetSdkVersion is set to 22, run-time permissions code will always return GRANTED for any permission, since permissions are granted at installation time via the manifest.
However, if your app is displaying runtime permissions dialogs, I'm guessing that you must be building against API 23 and using cordova-android#5+ for the Android platform.
Regarding permission groups vs individual permissions, you should read the Android documentation regarding runtime permissions:
The dialog box shown by the system describes the permission group your app needs access to; it does not list the specific permission. For example, if you request the READ_CONTACTS permission, the system dialog box just says your app needs access to the device's contacts. The user only needs to grant permission once for each permission group. If your app requests any other permissions in that group (that are listed in your app manifest), the system automatically grants them. When you request the permission, the system calls your onRequestPermissionsResult() callback method and passes PERMISSION_GRANTED, the same way it would if the user had explicitly granted your request through the system dialog box.
So in your case, requesting ACCESS_FINE_LOCATION grants access to all permissions in the LOCATION group (you can find the full list of groups and permissions here).
the Deny button also returns ...GRANTED, leaving the Location group permission off.
This should not return GRANTED. If the Deny button is pressed, access will be DENIED to that entire permission group (including the requested permission). The logic in your code snippet looks OK to handle this, so I would use the step-through debugger in Android Studio to see exactly what is happening in your code here.

Related

Android: how to use shouldShowRequestPermissionRationale to check permission state?

From the doc, shouldShowRequestPermissionRationale indicates whether you should show permission rationale UI.
I found:
if change one permission to "Notify" and before first showing the permission-request-dialog, shouldShowRequestPermissionRationale return false
if deny the permission with the "Never ask again" checkbox selected, shouldShowRequestPermissionRationale return false
Then I met a problem, how to distinguish these two cases? I mean, in the following code snippet from Android developer:
if (ContextCompat.checkSelfPermission(
CONTEXT, Manifest.permission.REQUESTED_PERMISSION) ==
PackageManager.PERMISSION_GRANTED) {
// You can use the API that requires the permission.
performAction(...);
} else if (shouldShowRequestPermissionRationale(...)) {
// In an educational UI, explain to the user why your app requires this
// permission for a specific feature to behave as expected. In this UI,
// include a "cancel" or "no thanks" button that allows the user to
// continue using your app without granting the permission.
showInContextUI(...);
} else {
// You can directly ask for the permission.
requestPermissions(CONTEXT,
new String[] { Manifest.permission.REQUESTED_PERMISSION },
REQUEST_CODE);
}
In the case 2, though denied the permission with "Never ask again", the code still invokes requestPermissions?

How to avoid requestPermission() if the user has checked "Never ask again"?

I'm dealing with on demand permissions in Android and I found a confussing situation. My objective is to display a dialog with a "go to app settings" button if the user has selected "Never ask again" in a permission.
OK, if the user selects "Never ask again" then, next time you request the permission you can know it doing this in the method onRequestPermissionsResult
public boolean checkIfShouldShowGoToSettingsDialog(String permissions[], int[] grantResults){
for (int i=0; i<permissions.length; i++){
if (grantResults[i] == PackageManager.PERMISSION_DENIED && !ActivityCompat.shouldShowRequestPermissionRationale(SectionManager.getInstance().getCurrentActivity(), permissions[i])) {
return true;
}
}
return false;
}
The problem is that if you do that, each time you call requestPermission(), then, onRequestPermissionsResult is being called again even with the permission denied previously and even with the checkbox selected. So checkIfShouldShowGoToSettingsDialog is returning true because the permission is DENIED and shouldShowRequestPermissionRationale is returning false. I don't want that. I want to display the "go to app settings" dialog only the first time the user has selected "Dont ask again".
So I tryed something:
I added this validation before calling requestPermission(). I tryed removing the permissions which returns false to the method shouldShowRequestPermissionRationale() because it's supossed that those permissions has the checkbox selected:
public String[] removePermanentlyDeniedPermissionsFromArray(String[] permissions){
List<String> result = new ArrayList<>();
for (String permission : permissions) {
if (ActivityCompat.shouldShowRequestPermissionRationale(SectionManager.getInstance().getCurrentActivity(), permission)) {
result.add(permission);
}
}
return result.toArray(new String[result.size()]);
}
It worked perfectly but... the problem is that the first time you open the app, the first time you are going to request the permissions... shouldShowRequestPermissionRationale() returns FALSE for all of them!! so no permissions are being requested.
Then, how can i avoid a callrequestPermission() if the user has checked "Never ask again" ?
I am very happy to answer your questions. In my past projects, I also encountered similar problems. The following is my solution.
(1) In order to avoid duplicate prompts, we must save a successful token when the user first agrees. Wait until the next operation, first determine whether the user has agreed, if you have agreed, then no longer prompt. Otherwise, prompt the user again.
(2) Here we need to use an open source framework for quickly saving reminder tags. Why use this framework? Because it has a method that can set the validity period of a tag directly. Specific usage can be viewed here: https://github.com/yangfuhai/ASimpleCache
So here is the wrong use of shouldShowPermissionRationale. That is true if permission was denied earlier (without clicking on check box). For first time it is always false. Check the doc
/**
* Gets whether you should show UI with rationale for requesting a permission.
* You should do this only if you do not have the permission and the context in
* which the permission is requested does not clearly communicate to the user
* what would be the benefit from granting this permission.
* <p>
* For example, if you write a camera app, requesting the camera permission
* would be expected by the user and no rationale for why it is requested is
* needed. If however, the app needs location for tagging photos then a non-tech
* savvy user may wonder how location is related to taking photos. In this case
* you may choose to show UI with rationale of requesting this permission.
* </p>
*
* #param activity The target activity.
* #param permission A permission your app wants to request.
* #return Whether you can show permission rationale UI.
*
* #see #checkSelfPermission(android.content.Context, String)
* #see #requestPermissions(android.app.Activity, String[], int)
*/
Process for requesting runtime permission
1.Generate the list of Permissions which are not granted.
List<String> permissionList = new ArrayList<>()
if(somePermission == PackageManager.PERMISSION_DENIED){
permissionList.add(somePermission);
}
//so on
String[] permissionArray = permissionList.toArray(new String[permissionArray.size()]);
2.In case of multiple permissions being asked at the same time, rationale might get complicated, So simply ignore it.
3.Request the permission with above generated Array.
4.In case permission is denied, redirect the user to settings screen if permission is mandatory for the app to run.
Best Practice:
Always group the permissions that are mandatory and optional within the app and ignore the rejections for optional permissions.

shouldShowRequestPermissionRationale always returns false

Currently I am programming an android app which reads out the phone number of the user. Because the app is developed for SDK 23+ (target sdk 27) I have to request the read sms permission. If the user denies the permission the first time a dialog should appear where the use of the phone number is explained. Then the user can choose to request the permission once more or to type the phone number manually. If the user tiks "never ask again" a dialog should appear where the user is instructed to allow the permission via the settings.
To check if the user has ticked never ask again, I use the method shouldShowRequestPermissionRationale. But the method always returns false, even if I never ticked never ask again.
Here is the relevant code:
public class SettingsSettingsFragment extends Fragment implements ActivityCompat.OnRequestPermissionsResultCallback {
public void readNumber() {
if (person.getPhoneNumber() == null || person.getPhoneNumber().equalsIgnoreCase("")) {
if (checkSelfPermission(getActivity(), READ_SMS) != PackageManager.PERMISSION_GRANTED)
requestPermission();
else {
//read phoneNumber
}
}
//request permission send sms
private void requestPermission() {
requestPermissions(new String[]{Manifest.permission.READ_SMS}, PERMISSION_REQUEST_SEND_SMS);
}
#Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_SEND_SMS)
if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
// user rejected the permission
boolean showRationale = shouldShowRequestPermissionRationale(Manifest.permission.SEND_SMS);
if (!showRationale) {
// user also CHECKED "never ask again" - show dialog
//show dialog: please allow in settings
} else if (counter < 2) {
// explain the permission, and give the user the possibility to ask once more
counter++;
}
}
Why is shouldShowRequestPermissionRationale always returning false?
The code is from here: Android M - check runtime permission - how to determine if the user checked "Never ask again"?
Thanks for help in advance.
According to android developers documentation of shouldShowRequestPermissionRationale():
method returns true if the app has requested this permission previously and the user denied the request.
If the user turned down the permission request in the past and chose the Don't ask again option in the permission request system dialog, this method returns false
If you haven't requested permission before it will return false because there is no need to pop the rational alert dialog.
This answer was very helpful for me to understand the issue.
Your requestPermission() is for READ_SMS, butshouldShowRequestPermissionRationale() is about SEND_SMS.
shouldShowRequestPermissionRationale() keeps returning false until user is asked for the "relevant" permission. Once requested, it returns true until user denies the same permission with "Never Ask Again" checked.
So, in your case, SEND_SMS is never requested. Therefore shouldShowRequestPermissionRationale(Manifest.permission.SEND_SMS) will keep returning false as expected. I made the same mistake before.

Android Marshmallow: How to allow Runtime Permissions programatically?

I'm developing an app that require some system permissions, however these are no longer granted automatically at installation time on Android Marshmallow.
I would like to request these permissions at runtime and run some kind of automation to grant them without needing a user to tap the 'allow' button when the System permissions Dialog appears.
How can I achieve this? Is there any way to do so in Marshmallow and later versions?
For Marshmallow or later permissions are not granted at install time and must be requested when required at runtime (if not granted previously.)
To do this, you need to run ActivityCompat.requestPermissions() to pop up the systems permissions dialog in your Activity, at the time when the user is undertaking an action that requires additional system permissions.
An example of this for the WRITE_EXTERNAL_STORAGE permission would be:
ActivityCompat.requestPermissions(
this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
WRITE_EXTERNAL_STORAGE_REQUEST_CODE
);
Note: WRITE_EXTERNAL_STORAGE_REQUEST_CODE is an arbitrary integer constant you should define elsewhere.
The permissions you request should also be declared in your AndroidManifest.xml. In this example the declaration would be:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
In order to handle the system permissions dialog response you will also need to implement onRequestPermissionsResult() in your Activity. For this example the code would be similar to
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String permissions[], #NonNull int[] grantResults) {
if (grantResults.length == 0 || grantResults[0] == PackageManager.PERMISSION_DENIED) {
return; //permission not granted, could also optionally log an error
}
if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST_CODE) {
//Do whatever you needed the write permissions for
}
}
If you are automating your app through Espresso, UIAutomator and/or some other UI testing framework you will need to anticipate and click the system dialog during your test, which can be accomplished with the following test code:
private void allowPermissionsIfNeeded() {
if (Build.VERSION.SDK_INT >= 23) {
UiObject allowPermissions = mDevice.findObject(new UiSelector().text("Allow"));
if (allowPermissions.exists()) {
try {
allowPermissions.click();
} catch (UiObjectNotFoundException e) {
Timber.e(e, "There is no permissions dialog to interact with ");
}
}
}
}
A more comprehensive explanation of testing System UI Permissions is available here.
I found out that an simpler way to automate the permission acceptance without using UIAutomator or espresso in a CI scenario is to simply, pre-installing the apk via adb using:
adb install -g {my_apk_file}
The -g flag automatically grants all manifest permissions to the app. Afterwards if you launch your espresso test suite, the ui won't ask you again to grant them.

Is ACCESS_COARSE_LOCATION necessary if the user has already granted ACCESS_FINE_LOCATION?

I am updating my app to work with the new Android Marshmallow permission framework and it looks like it's enough for the user to grant the ACCESS_FINE_LOCATION permission at runtime for the app to work fine. This is what I do:
public static final String[] runtimePermissions = { permission.ACCESS_FINE_LOCATION };
public static final int LOCATION_PERMISSION_IDENTIFIER = 1;
and further down the class:
public static boolean checkConnectionPermission(final Context context) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (context.checkSelfPermission(permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
return true;
}
else {
((Activity) context).requestPermissions(runtimePermissions,
LOCATION_PERMISSION_IDENTIFIER);
return false;
}
}
// since in API levels below M the permission is granted on
// installation,
// it is considered a given that the permission has been granted since
// the
// app is running. So we return true by default
else {
return true;
}
}
I am just concerned that I am overlooking something that could cause trouble in the future (the app is near-production) with Security Exception(s).
I guess my ultimate question is: does granting FINE_LOCATION somehow auto-grant COARSE_LOCATION too?
Sort of already answered here.
If I have ACCESS_FINE_LOCATION already, can I omit ACCESS_COARSE_LOCATION?
Some additional information, you should look at permission group.
https://developer.android.com/preview/features/runtime-permissions.html
They are in the same permission group. If you really want to play safe, just include both in your manifest, and request them on need. It would be transparent to user.
According to this blog post, if you have specified both in the manifest and the user has granted you one, then when you ask for the other it will be automatically granted (since they're both in the same permission group).
That means that if you have been granted COARSE_LOCATION and then ask for FINE_LOCATION you can get it without the user being prompted, but the catch is that you still have to specify both in the manifest.
Apps can actually register for two types of location updates. Fine grained, which uses the Global Positioning System to get the device’s location accurate to within a few meters, vs coarse-grained, which uses the Wifi, Cell towers and other data available to the device to get a rough (accurate to 10s of meters) location of the device.
Choice is yours.

Categories

Resources