我们如何在 Android M 的运行时权限中区分从未询问过和停止询问?

How Do We Distinguish Never-Asked From Stop-Asking in Android M's Runtime Permissions?

当谈到M Developer Preview 运行时候的权限,据Google

  1. 如果您以前从未请求过某个权限,请直接请求

  2. 如果你之前问过,用户说 "no",然后用户尝试做一些需要拒绝权限的事情,你应该提示用户解释你为什么需要权限, 在您再次请求权限之前

  3. 如果您之前问过几次,并且用户已经说 "no, and stop asking"(通过 运行 时间权限对话框上的复选框),您应该停止打扰(例如,禁用需要权限的UI)

但是,我们只有一个方法,shouldShowRequestPermissionRationale(),返回一个boolean,我们有三个状态。我们需要一种方法来区分从未询问的状态和停止询问的状态,因为我们从 shouldShowRequestPermissionRationale() 得到 false 两者。

对于应用程序首次 运行 请求权限,这不是大问题。有很多方法可以确定这可能是您应用程序的第一个 运行(例如,SharedPreferences 中的 boolean 值),因此您假设如果它是第一个 运行 你的应用程序,你处于从未被询问的状态。

但是,运行时间权限的部分愿景是您可能不会预先要求所有权限。当用户点击需要该权限的内容时,您可能只会在以后请求与边缘功能相关的权限。在这里,应用程序可能已经 运行 好几个月了,然后我们突然需要请求另一个权限。

在那些情况下,我们是否应该跟踪我们是否自己请求了许可?或者 Android M API 中是否有我遗漏的东西告诉我们我们之前是否询问过?

根据当前示例: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
        }
    }

在 SharedPreferences 中存储一个布尔值,密钥作为您的权限代码,值如上所述,以指示该偏好之前是否已被拒绝。

遗憾的是,当您的应用 运行 时,您可能无法检查已被接受但后来被拒绝的偏好。最终规范不可用,但您的应用有可能会重新启动或获取模拟值,直到下一次启动。

你可以看看here - 有一张流程图很好地解释了这个过程。它还解释了何时应调用 shouldShowRequestPermissionRationale() 以及何时 return 为真。

基本上根据 Android 的文档,如果您没有许可,您应该始终请求许可(Android 将在回调中自动 return DENIED 如果用户说不再询问)并且如果用户在过去已经拒绝过一次但没有标记不再询问选项,则您应该显示一条短消息。

不,你不需要跟踪你是否请求了权限,你也不需要区分 Never-Asked 和 Stop-Asking。

状态 1 和 3 对应用程序开发者来说是相同的:您需要权限和 ActivityCompat.checkSelfPermission != PackageManager.PERMISSION_GRANTED,然后只要用户点击需要权限的功能,您就可以通过 ActivityCompat.requestPermissions() 请求权限许可,无论您请求了多少次。用户最终会"Grant"它,或者"Deny"它并选中"never ask again"。该设计不会阻止您多次弹出权限请求对话框。

但是,该设计确实鼓励您在某些时候解释权限的目的 - 您的状态 2。shouldShowRequestPermissionRationale() 不用于确定您是否应该请求权限,它用于确定您是否在您请求许可之前应该显示解释。

关于状态 3 的更多解释:

  1. 是的,我们应该通过停止显示解释而不是停止请求来停止打扰用户。这就是为什么他们提供了 shouldShowRequestPermissionRationale().
  2. 保留请求权限并不麻烦。用户选择"never ask again"后,ActivityCompat.requestPermissions()将不再弹出对话框。
  3. 最好在单用户会话期间每次发现我们没有权限时禁用相关的 UI。而不是在 shouldShowRequestPermissionRationale() return 之后禁用 UI false.

我有一个方法可以解决你的问题,它对我来说似乎很有效。

我使用 SharedPreferences 区分 Never-Asked 和 Stop-Asking,我将举例说明我是如何使用它的。

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

无需为权限状态创建并行持久化状态,您可以随时使用returns当前权限状态的这种方法:

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

警告:returns 在用户 accepted/denied 通过用户提示获得许可之前,returns 阻止了第一个应用程序启动(在 sdk 23+ 设备上)

我知道我发帖很晚了,但是详细的例子可能对某人有帮助。

我注意到,如果我们在 onRequestPermissionsResult() 回调方法中检查 shouldShowRequestPermissionRationale() 标志,它只显示两种状态。

状态 1:-Return true:-- 任何时候用户单击“拒绝权限”(包括第一次。

状态 2:-Returns false :- 如果用户 select s“不再询问。

这是一个有多个权限请求的例子:-

该应用程序在启动时需要 2 个权限。 SEND_SMS 和 ACCESS_FINE_LOCATION(都在 manifest.xml 中提到)。

应用程序一启动,就会同时请求多个权限。如果授予这两个权限,则正常流程进行。

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

如果一个或多个权限未被授予, activityCompat.requestPermissions() 将请求权限,控制转到 onRequestPermissionsResult() 回调方法。

您应该检查 onRequestPermissionsResult() 回调方法中 shouldShowRequestPermissionRationale() 标志的值。

只有两种情况:--

案例 1:- 任何时候用户单击拒绝权限(包括第一次),它将 return 为真。所以当用户否认的时候,我们可以给出更多的解释,再继续询问。

情况 2:-只有当用户 select“不再询问”时才会 return 错误。在这种情况下,我们可以继续使用有限的功能并引导用户从设置中激活权限以获得更多功能,或者如果权限对于应用程序来说微不足道,我们可以完成设置。

案例- 1

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

这是跟踪第一次显示权限对话框的方法,当用户选中不再询问以及在用户选中不再询问后直接拒绝权限时,我们需要保留一个标志用于权限理由对话框已在 onRequestPermissionsResult 中获得结果之前显示。 需要时调用方法 checkPermission()。

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

在尝试了此处的所有答案以及互联网上的其他一些 post 之后。我开始知道我必须使用 sharedPreference isLocationPermissionDialogShown(默认为 false)并且每件事都按预期进行。

  1. 如果是第一次请求许可。在这种情况下 shouldShowRequestPermissionRationale returns falseisLocationPermissionDialogShownfalse
  2. 第二次 shouldShowRequestPermissionRationale return true 并且在显示对话框时我们将 isLocationPermissionDialogShown 设置为 true。当我们检查条件时,两者都是 true
  3. 勾选每次直到不再询问 shouldShowRequestPermissionRationale return trueisLocationPermissionDialogShown returns true
  4. 如果不再询问已勾选 shouldShowRequestPermissionRationale return falseisLocationPermissionDialogShown returns true。这就是我们需要的。

请检查工作示例。

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

}

希望这会有所帮助。


SO FINALLY MY TIME HAS COME TO ANSWER A QUESTION FROM COMMONSWARE


业务流程:-

1. 当用户第一次点击"deny permission"时,我会显示基本原理对话框来解释权限的必要性。然后,如果用户单击基本原理对话框上的 "cancel" 按钮,我将显示消息 "Please give permission to get location".

2。 之后,当用户在权限对话框中点击拒绝权限(不再询问)时,我会显示一条消息 "Please give location permission from app settings"。请注意,我添加了单词 "from app settings",因为用户选中了 "dont ask again" 的复选框。

3。 因此从现在开始,权限对话框将不会 shown.Also 基本原理对话框将不会显示。

所以这里的关键是,如果权限对话框和基本原理对话框都没有显示,那么这意味着用户已经选中了"dont ask again"复选框。

代码:-

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

检查这个存储库: https://github.com/debChowdhury/PermissionHelperEasy


Easy peasy


关于MLProgrammer-CiM的回答,我有一个想法是如何解决SharedPrefrences中存储的boolean已经为真后用户撤销权限的场景,

只需创建另一个常量布尔值,如果第一个调用例如:Constant.FIRST_TIME_REQUEST(其默认状态为真) 第二个将被称为 Constant.PERMISSION_ALREADY_GRANTED(默认情况下为 false)

onRequestPermissionsResult 上,如果获得许可,您当然会将其值更改为 true。

现在,在你想要预先解释的地方,写下这样的东西:

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

这样即使用户删除权限,布尔值也会再次设置为 false。

祝你好运,希望对你有所帮助。