When it comes to the M Developer Preview runtime permissions, according to Google:
If you have never asked for a certain permission before, just ask for it If you asked before, and the user said "no", and the user then tries doing something that needs the rejected permission, you should prompt the user to explain why you need the permission, before you go on to request the permission again If you asked a couple of times before, and the user has said "no, and stop asking" (via the checkbox on the runtime permission dialog), you should just stop bothering (e.g., disable the UI that requires the permission)
However, we only have one method, shouldShowRequestPermissionRationale()
, returning a boolean
, and we have three states. We need a way to distinguish the never-asked state from the stop-asking state, as we get false
from shouldShowRequestPermissionRationale()
for both.
For permissions being requested on first run of the app, this is not a big problem. There are plenty of recipes for determining that this is probably the first run of your app (e.g., boolean
value in SharedPreferences
), and so you assume that if it's the first run of your app, you're in the never-asked state.
However, part of the vision of runtime permissions is that you might not ask for all of them up front. Permissions tied to fringe features you might only ask for later on, when the user taps on something that requires that permission. Here, the app may have been run many times, for months, before we all of a sudden need to request another permission.
In those cases, are we supposed to track whether or not we asked for the permission ourselves? Or is there something in the Android M API that I am missing that tells us whether we asked before or not?
SharedPreferences
getting out of sync with Android's own stored permission information. Android is the "system of record" with regards to runtime permissions. It clearly has the information, as otherwise it could not ever return true
from shouldShowRequestPermissionRationale()
. I'm just seeing if there's some method that got added that I'm missing, that's all.
shouldShowRequestPermissionRationale()
in 6.1 and add a new method that returns an int
.
I know I am posting very late, but detailed example may be helpful for someone.
What I have noticed is, if we check the shouldShowRequestPermissionRationale() flag in to onRequestPermissionsResult() callback method, it shows only two states.
State 1:-Return true:-- Any time user clicks Deny permissions (including the very first time.
State 2:-Returns false :- if user select s “never asks again.
Here is an example with multiple permission request:-
The app needs 2 permissions at startup . SEND_SMS and ACCESS_FINE_LOCATION (both are mentioned in manifest.xml).
As soon as the app starts up, it asks for multiple permissions together. If both permissions are granted the normal flow goes.
https://i.stack.imgur.com/TmOCS.png
public static final int REQUEST_ID_MULTIPLE_PERMISSIONS = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(checkAndRequestPermissions()) {
// carry on the normal flow, as the case of permissions granted.
}
}
private boolean checkAndRequestPermissions() {
int permissionSendMessage = ContextCompat.checkSelfPermission(this,
Manifest.permission.SEND_SMS);
int locationPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION);
List<String> listPermissionsNeeded = new ArrayList<>();
if (locationPermission != PackageManager.PERMISSION_GRANTED) {
listPermissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (permissionSendMessage != PackageManager.PERMISSION_GRANTED) {
listPermissionsNeeded.add(Manifest.permission.SEND_SMS);
}
if (!listPermissionsNeeded.isEmpty()) {
ActivityCompat.requestPermissions(this, listPermissionsNeeded.toArray(new String[listPermissionsNeeded.size()]),REQUEST_ID_MULTIPLE_PERMISSIONS);
return false;
}
return true;
}
In case one or more permissions are not granted, activityCompat.requestPermissions() will request permissions and the control goes to onRequestPermissionsResult() callback method.
You should check the value of shouldShowRequestPermissionRationale() flag in onRequestPermissionsResult() callback method.
There are only two cases:--
Case 1:-Any time user clicks Deny permissions (including the very first time), it will return true. So when the user denies, we can show more explanation and keep asking again.
Case 2:-Only if user select “never asks again” it will return false. In this case, we can continue with limited functionality and guide user to activate the permissions from settings for more functionalities, or we can finish the setup, if the permissions are trivial for the app.
CASE- 1
https://i.stack.imgur.com/nbTo6.png
CASE- 2
https://i.stack.imgur.com/rQqYp.png
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
Log.d(TAG, "Permission callback called-------");
switch (requestCode) {
case REQUEST_ID_MULTIPLE_PERMISSIONS: {
Map<String, Integer> perms = new HashMap<>();
// Initialize the map with both permissions
perms.put(Manifest.permission.SEND_SMS, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
// Fill with actual results from user
if (grantResults.length > 0) {
for (int i = 0; i < permissions.length; i++)
perms.put(permissions[i], grantResults[i]);
// Check for both permissions
if (perms.get(Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "sms & location services permission granted");
// process the normal flow
//else any one or both the permissions are not granted
} else {
Log.d(TAG, "Some permissions are not granted ask again ");
//permission is denied (this is the first time, when "never ask again" is not checked) so ask again explaining the usage of permission
// // shouldShowRequestPermissionRationale will return true
//show the dialog or snackbar saying its necessary and try again otherwise proceed with setup.
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.SEND_SMS) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
showDialogOK("SMS and Location Services Permission required for this app",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
checkAndRequestPermissions();
break;
case DialogInterface.BUTTON_NEGATIVE:
// proceed with logic by disabling the related features or quit the app.
break;
}
}
});
}
//permission is denied (and never ask again is checked)
//shouldShowRequestPermissionRationale will return false
else {
Toast.makeText(this, "Go to settings and enable permissions", Toast.LENGTH_LONG)
.show();
// //proceed with logic by disabling the related features or quit the app.
}
}
}
}
}
}
private void showDialogOK(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", okListener)
.create()
.show();
}
As per the current example: https://github.com/googlesamples/android-RuntimePermissions/blob/master/Application/src/main/java/com/example/android/system/runtimepermissions/MainActivity.java#L195
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
if (requestCode == REQUEST_CAMERA) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
doThing();
//STORE FALSE IN SHAREDPREFERENCES
} else {
//STORE TRUE IN SHAREDPREFERENCES
}
}
Store a boolean in SharedPreferences with key as your permission code and value as indicated above, to indicate whether that preference has been denied before.
Sadly, you probably can't check against a preference that has been accepted and later denied while your app is running. The final spec is not available, but there's a chance that your app either gets restarted or gets mock values until the next launch.
No, you don't need to track whether or not you asked for the permission, and you don't need to distinguish Never-Asked From Stop-Asking.
The state 1 and 3 are the same for app developer: you need the permission and ActivityCompat.checkSelfPermission != PackageManager.PERMISSION_GRANTED
, then you just ask for the permission via ActivityCompat.requestPermissions()
, whenever user tapped the feature that requires the permission, no matter how many times you have requested. User will eventually "Grant" it, or "Deny" it with "never ask again" checked. The design does NOT discourage you from popup the permission request dialogbox multiple times.
However, the design does encourage you to explain the purpose of the permission at some point - your state 2. shouldShowRequestPermissionRationale()
is NOT used to determine if you should request for permission, it's used to determine if you should show explanations, BEFORE you request for permission.
A couple more explanation regarding the state 3:
Yes, we should stop bothering user by stop showing the explanation, not stop request. That's why they provided the shouldShowRequestPermissionRationale(). It's not bothering to keep request for permission. After user chose "never ask again", ActivityCompat.requestPermissions() will not popup dialogbox anymore. It's better to disable the relevant UI every time we find out we don't have the permission, during single user session. Instead of disable the UI after shouldShowRequestPermissionRationale() return false.
shouldShowRequestPermissionRationale()
seems to be returning the expected value already in onRequestPermissionsResult()
, so if the user denied with don't-ask-again, shouldShowRequestPermissionRationale()
does indeed return false
. So if you want to have the same response (e.g., show a snackbar) regardless of whether the user just denied with don't-ask-again or did so previously, you do not need state 1. If you want different responses (e.g., only show a snackbar if the user denied with don't-ask-again some time ago, not just now), you would still need state 1.
shouldShowRequestPermissionRationale()
will return false
if you never showed the permission dialog before, even if the user didn't denied the permission ever before. So the first time you should force the permission dialog and then you can rely on this. Take in account that this behavior can change in the future.
I have an approach to the solution to your problem, it seems to work pretty well for me.
I Distinguish Never-Asked From Stop-Asking using the SharedPreferences, I'll give you an example of how I use that.
private void requestAccountPermission() {
SharedPreferences mPreferences = getSharedPreferences("configuration", MODE_PRIVATE);
boolean firstTimeAccount = mPreferences.getBoolean("firstTimeAccount", true);
if (ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.GET_ACCOUNTS)) {
// 2. Asked before, and the user said "no"
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.GET_ACCOUNTS}, REQUEST_CODE_ACCOUNTS);
}else {
if(firstTimeAccount) {
// 1. first time, never asked
SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("firstTimeAccount", false);
editor.commit();
// Account permission has not been granted, request it directly.
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.GET_ACCOUNTS},REQUEST_CODE_ACCOUNTS);
}else{
// 3. If you asked a couple of times before, and the user has said "no, and stop asking"
// Your code
}
}
}
Here is method to track when permission dialog was shown first time, when user checked the never ask again and when permission is directly denied after user checked never ask again for this we need to keep a flag for if permission rationale dialog has been shown before getting result in onRequestPermissionsResult. Call method checkPermission() when required.
public boolean mPermissionRationaleDialogShown = false;
public void checkPermission() {
if (ContextCompat.checkSelfPermission(this, "PermissionName")
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, "PermissionName")) {
showPermissionRequiredDialog();
} else {
askPermission();
}
} else {
// Permission Granted
}
}
public void askPermission() {
ActivityCompat.requestPermissions(this,
new String[]{"PermissionName"}, permissionRequestCode);
}
public void showPermissionRequiredDialog() {
mPermissionRationaleDialogShown = true;
// Dialog to show why permission is required
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission Granted
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, "PermissionName")
&& !mPermissionRationaleDialogShown) {
// Permission dialog was shown for first time
} else if (ActivityCompat.shouldShowRequestPermissionRationale(this, "PermissionName")
&& mPermissionRationaleDialogShown){
// User deny permission without Never ask again checked
} else if (!ActivityCompat.shouldShowRequestPermissionRationale(this, PERMISSION_READ_EXTERNAL)
&& mPermissionRationaleDialogShown) {
// User has checked Never ask again during this permission request
} else {
// No permission dialog shown to user has user has previously checked Never ask again. Here we can show dialog to open setting screen to change permission
}
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
After trying all the answer here and some other post over internet. I came to know that I have to use a sharedPreference isLocationPermissionDialogShown
(default false) and every thing works as per expected.
If first time asked for permission. In this case shouldShowRequestPermissionRationale returns false and isLocationPermissionDialogShown also false Second time shouldShowRequestPermissionRationale return true and while showing dialog we set isLocationPermissionDialogShown to true. and when we check condition both will be true Every Time until Never Ask Again ticked shouldShowRequestPermissionRationale return true and isLocationPermissionDialogShown returns true If Never Ask Again ticked shouldShowRequestPermissionRationale return false and isLocationPermissionDialogShown returns true. Which is what we need.
Please check working example.
public class MainActivity extends AppCompatActivity {
SharedPreferences sharedPreferences;
String locationPermission;
String prefLocationPermissionKey = "isLocationPermissionDialogShown";
private final int PERMISSION_REQUEST_CODE_LOCATION = 1001;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
locationPermission = Manifest.permission.ACCESS_FINE_LOCATION;
sharedPreferences = getSharedPreferences("configuration", MODE_PRIVATE);
//check for android version
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//Check for permission
if (checkSelfPermission(locationPermission) != PackageManager.PERMISSION_GRANTED) {
//check if clarification dialog should be shown.
if (shouldShowRequestPermissionRationale(locationPermission)) {
showClarificationDialog(locationPermission, PERMISSION_REQUEST_CODE_LOCATION);
} else {
requestPermissions(new String[] { locationPermission}, PERMISSION_REQUEST_CODE_LOCATION);
}
} else {
Log.d("nets-debug", "permission already grranted");
}
}
}
@Override
@TargetApi(Build.VERSION_CODES.M)
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
//for location permission
if (requestCode == PERMISSION_REQUEST_CODE_LOCATION) {
boolean isLocationPermissionDialogShown = sharedPreferences.getBoolean(prefLocationPermissionKey, false);
if (!shouldShowRequestPermissionRationale(locationPermission) && isLocationPermissionDialogShown) {
// user selected Never Ask Again. do something
Log.d("nets-debug", "never ask again");
} else {
// all other conditions like first time asked, previously denied etc are captured here and can be extended if required.
Log.d("nets-debug", "all other cases");
}
}
}
}
@TargetApi(Build.VERSION_CODES.M)
public void showClarificationDialog(final String permission, final int requestCode) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Permission Required");
builder.setMessage("Please grant Location permission to use all features of this app");
builder.setPositiveButton("Grant", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(prefLocationPermissionKey, true);
editor.apply();
requestPermissions(new String[] {permission}, requestCode);
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(getApplicationContext(), "This permission required", Toast.LENGTH_LONG).show();
}
});
builder.create().show();
}
}
Hope this will help.
Regarding MLProgrammer-CiM's answer, I have an idea of how to solve the scenario in which the user revokes the permission after the boolean stored in SharedPrefrences is already true,
simply create another constant boolean, if the first one called for example: Constant.FIRST_TIME_REQUEST
(which its default state will be true) the second one will be called Constant.PERMISSION_ALREADY_GRANTED
(which will be false on default)
On onRequestPermissionsResult
if permission was granted you change its value to true, of course.
Now, in the part where you want to ask permission with pre-explanation, write something like that:
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
boolean isPermissionGranted = sp.getBoolean(Constant.PERMISSION_ALREADY_GRANTED, false);
if (isPermissionGranted) {
sp.putBoolean(Constant.PERMISSION_ALREADY_GRANTED, false);
sp.putBoolean(Constant.FIRST_TIME_REQUEST, true);
}
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, androidPermissionName) || sp.getBoolean(Constant.FIRST_TIME_REQUEST, true) ) {
showDialogExplanation();
}
}
that way even if the user will remove the permission, the boolean will be set to false again.
good luck, I hope it'll help.
Shlo
SO FINALLY MY TIME HAS COME TO ANSWER A QUESTION FROM COMMONSWARE
Business flow:-
1. When user clicks on "deny permission" for the first time, I will show rationale dialog to explain the necessity of the permission. Then if the user clicks on "cancel" button on the rationale dialog, I will show a toast showing message "Please give permission to get location".
2. After that when user clicks on deny permission(dont ask again) on the permissions dialog, I will show a message "Please give location permission from app settings". Notice that I have added the words "from app settings" because the user has checked the box of "dont ask again".
3. So from now on the permission dialog will not be shown.Also the rationale dialog will not be shown.
So the key here is that if both permission dialog and rationale dialog is not shown, then it means that user has checked the "dont ask again" checkbox.
The code:-
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.ACCESS_FINE_LOCATION)){
AlertDialogHelper.showDialogWithYesNoCallback(mContext, getString(R.string.confirm), getString(R.string.please_give_permission_to_get_location), new onItemClickReturnBoolean() {
@Override
public void onItemClick(Boolean status) {
if(status){
ActivityCompat.requestPermissions(SplashScreenActivity.this,permissions,AppConfig.FINE_LOCATION_PERMISSION_REQUEST_CODE);
}
else{
ShowToast.showShortToast(SplashScreenActivity.this,getString(R.string.please_give_permission_to_get_location));
finish();
}
}
});
}
else{
ActivityCompat.requestPermissions(this,permissions,AppConfig.FINE_LOCATION_PERMISSION_REQUEST_CODE);
}
}
else{
gettingLocationAfterPermissionGranted();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == AppConfig.FINE_LOCATION_PERMISSION_REQUEST_CODE){
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
gettingLocationAfterPermissionGranted();
}
else{
if(ActivityCompat.shouldShowRequestPermissionRationale(SplashScreenActivity.this,Manifest.permission.ACCESS_FINE_LOCATION)){
ShowToast.showShortToast(this,getString(R.string.please_give_permission_to_get_location));
}
else{
ShowToast.showShortToast(this,getString(R.string.please_give_location_permission_from_app_settings));
}
finish();
}
}
}
Check this repository: https://github.com/debChowdhury/PermissionHelperEasy
Easy peasy
You can look here - there is a flowchart that explains the process quite good. It also explains when you should call shouldShowRequestPermissionRationale()
and when it returns true.
Basically according to Android's documentation, you should always ask for permission if you don't have it (Android will automatically return DENIED in the callback if the user said to never ask again) and you should display a short message if the user has already declined you once in the past but hasn't marked the never ask again option.
There is no need to create a parallel persistent state for the permission state, you could just use this method which returns the current permission state at any time:
@Retention(RetentionPolicy.SOURCE)
@IntDef({GRANTED, DENIED, BLOCKED})
public @interface PermissionStatus {}
public static final int GRANTED = 0;
public static final int DENIED = 1;
public static final int BLOCKED = 2;
@PermissionStatus
public static int getPermissionStatus(Activity activity, String androidPermissionName) {
if(ContextCompat.checkSelfPermission(activity, androidPermissionName) != PackageManager.PERMISSION_GRANTED) {
if(!ActivityCompat.shouldShowRequestPermissionRationale(activity, androidPermissionName)){
return BLOCKED;
}
return DENIED;
}
return GRANTED;
}
Caveat: returns BLOCKED the first app start, before the user accepted/denied the permission through the user prompt (on sdk 23+ devices)
I also used this answered here.
getPermissionStatus
will incorrectly return BLOCKED
, which is actually not true. A 4th state is required in this design, probably called HAVENT_ASKED
, and the only way to detect that is through use of a shared pref or something similar.
Success story sharing
for (int i = 0; i < permissions.length; i++)