How Do We Distinguish Never-Asked From Stop-Asking

2020-01-25 14:02发布

问题:

When it comes to the M Developer Preview runtime permissions, according to Google:

  1. If you have never asked for a certain permission before, just ask for it

  2. 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

  3. 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?

回答1:

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.



回答2:

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.

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

CASE- 2

@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();
    }


回答3:

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:

  1. Yes, we should stop bothering user by stop showing the explanation, not stop request. That's why they provided the shouldShowRequestPermissionRationale().
  2. It's not bothering to keep request for permission. After user chose "never ask again", ActivityCompat.requestPermissions() will not popup dialogbox anymore.
  3. 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.


回答4:

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
            }
        }
    }


回答5:

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);
    }
}


回答6:

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.

  1. If first time asked for permission. In this case shouldShowRequestPermissionRationale returns false and isLocationPermissionDialogShown also false
  2. Second time shouldShowRequestPermissionRationale return true and while showing dialog we set isLocationPermissionDialogShown to true. and when we check condition both will be true
  3. Every Time until Never Ask Again ticked shouldShowRequestPermissionRationale return true and isLocationPermissionDialogShown returns true
  4. 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.



回答7:


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




回答8:

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.



回答9:

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.