Introduction to the

For permissions, every Android developer should be familiar with the need to dynamically apply for sensitive permissions such as access to address book permissions, camera permissions, location permissions, etc. when targetSDK is greater than 23. In Android 6.0, the concept of permission group is also added. If the user agrees to a certain permission in the group, the system default APP can use all the permissions in the group without applying again. Here’s a picture of the permissions group:

Application Permission API

First introduce the process of applying for permissions dynamically above Android 6.0. When applying for permissions, users can click reject and choose not to be reminded when applying again. The following describes the API used to apply for permissions at runtime, using kotlin as a code example

  • Register in Manifest
	<uses-permission android:name="android.permission.XXX"/>
Copy the code
  • Check whether the user agrees to a permission
	// (API) int checkSelfPermission (Context context, String permission)ContextCompat.checkSelfPermission(context, Manifest.permission.XXX) ! = PackageManager.PERMISSION_GRANTEDCopy the code
  • To apply for permission
	// (API) void requestPermissions (Activity activity, String[] permissions, int requestCode)
   requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_CODE_CALL_PHONE)

Copy the code
  • Request a result callback
	// (API) void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)
	override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray){}Copy the code
  • Whether the purpose of requesting permission needs to be explained to the user
	// (API) boolean shouldShowRequestPermissionRationale (Activity activity, String permission)
	ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)
Copy the code
situation The return value
The first time you open your App false
Last time the popup permissions click disable (but did not check “next time not to ask”) true
Select Disable last time and check “Next time not to ask” false

Note: This method returns false if the user has rejected permission requests in the past and selected the Don’t Ask Again option in the permission Request system dialog box. This method also returns false if the device specification forbids the application from having this permission.

Single permission application interaction process

We need to directly interact with users in mobile terminals, and we need to think more about how to interact with users to achieve the best experience. Below I combine the example of dynamically applying for permissions from Google Samples android-Runtimepermissions github.com/googlesampl… And dynamic application permission framework easypermissions github.com/googlesampl… So just to summarize the interaction.

First of all, Android does not recommend the App to directly dial such sensitive operations, it is recommended to jump to the dial interface and put the phone number into the dial interface. Here is only for reference. In each case, the user applies for permission for the first time (permission query status).

  • Direct permission.


  • Refuse and then apply for permission again


  • No more reminders to guide to the Settings screen

Without further ado, go to the code.

    /** * creates a companion object that provides a static variable */
    companion object {
        const val TAG = "MainActivity"
        const val REQUEST_CODE_CALL_PHONE = 1}...// Call requestPermmission() to request permission before dialing.private fun callPhone(a) {
        val intent = Intent(Intent.ACTION_CALL)
        val data = Uri.parse("tel:9898123456789")
        intent.data = data
        startActivity(intent)
    }

    /** * Prompts the user to apply for permission description */
    @TargetApi(Build.VERSION_CODES.M)
    fun showPermissionRationale(rationale: String) {
        Snackbar.make(view, rationale,
                Snackbar.LENGTH_INDEFINITE)
                .setAction("Sure") {
                    requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
                }.setDuration(3000)
                .show()
    }


    /** * Users click the dial button to apply for permission */
    private fun requestPermmission(context: Context) {

        // Determine whether the runtime permission request is required
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) ! = PackageManager.PERMISSION_GRANTED) {// Determine whether the user needs to be reminded. If the user has clicked reject && and does not check the box, the user will be reminded
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
                // Give to give permission to explain, for the situation that has been rejected, first prompt application reason, then apply for
                showPermissionRationale("You need to open the phone permission to directly dial the phone, convenient for your operation.")}else {
                // Proceed directly without giving reasons. If the user uses this function for the first time (applying for permission for the first time), the user rejects the permission and selects "Do not remind"
                // Place the bootjump setup operation in the request result callback
                requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
            }
        } else {
            // Have permission to make function calls directly
            callPhone()
        }
    }

    /** * Permission request callback */
    @TargetApi(Build.VERSION_CODES.M)
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        // According to requestCode, this is the callback for that permission request
        if (requestCode == REQUEST_PERMISSION_CODE_CALL_PHONE) {
            // Determine whether the user agrees to the request
            if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                callPhone()
            } else {
                // No consent
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
                    // Give to give permission to explain, for the situation that has been rejected, first prompt application reason, then apply for
                    showPermissionRationale("You need to open the phone permission to directly dial the phone, convenient for your operation.")}else {
                    // If the user has selected "No reminding", the user will be guided to enter the setting interface to enable the permission
                    Snackbar.make(view, "Permissions need to be turned on to use this feature, you can also go to Settings -> Applications... Enable permission",
                            Snackbar.LENGTH_INDEFINITE)
                            .setAction("Sure") {
                                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                                intent.data = Uri.parse("package:$packageName")
                                startActivityForResult(intent,REQUEST_SETTINGS_CODE)
                            }
                            .show()
                }
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }
    
    public override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_SETTINGS_CODE) {
            Toast.makeText(this."Decide again if you agree to the permission, and then customize it.",
                    Toast.LENGTH_LONG).show()
        }
    }

  }

Copy the code

EasyPermissions usage and problems

The application of single authority is introduced above. The amount of code for a simple application is not small, and the need for multiple permissions for a function requires complex logical judgment. Google has introduced an open source framework for permission application, which is explained around EasyPermission below. I won’t introduce how to use it, but I’ll just take a look at the demo. There are also many articles on the Internet quoting previous summaries.

Blog.csdn.net/hexingen/ar…

When I was in use found there is a problem, use version is pub devrel: easypermissions: 2.0.0, when using multiple permissions in the demo application to agree to a, refused to a, no check is not remind. At this time, when applying for permission for the second time, click “Cancel” when prompting the user to use the permission, the pop-up box for setting manual opening will pop up. This approach is not appropriate, the user did not click not to remind, can guide the user authorization inside the APP, there must be something wrong with the logic. The first map









Conclusion first, the following method is entered when the user is prompted to click Cancel


    @Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());

        // (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
        // This will display a dialog directing them to enable the permission in app settings.
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show(); }}Copy the code

In judgingEasyPermissions.somePermissionPermanentlyDenied()A dialog is displayed when a problem is detected.

EasyPermissions source code analysis

Here I will follow the ideas used by demo, read the source code. We recommend downloading the source code, which has a link to call the following method after clicking the two permission buttons

    @AfterPermissionGranted(RC_LOCATION_CONTACTS_PERM)
    public void locationAndContactsTask(a) {
        if (hasLocationAndContactsPermissions()) {
            // If you have permission, toast
            Toast.makeText(this."TODO: Location and Contacts things", Toast.LENGTH_LONG).show();
        } else {
            // If there is no permission, apply for permission and submit it to EasyPermission class for management
            EasyPermissions.requestPermissions(
                    this, getString(R.string.rationale_location_contacts), RC_LOCATION_CONTACTS_PERM, LOCATION_AND_CONTACTS); }}Copy the code

Follow the use of the idea of sorting, regardless of the annotations. Follow up EasyPermissions requestPermissions

    /** * Request multiple permissions, if the system needs to pop up permission description **@param host        context
     * @paramRationale wants the user to explain why you need these permissions *@paramRequestCode request code which is used in onRequestPermissionsResult callback to determine a * application@paramPerms specifies the required permissions */
    public static void requestPermissions(
            @NonNull Activity host, @NonNull String rationale,
            int requestCode, @Size(min = 1) @NonNull String... perms) {
        requestPermissions(
                new PermissionRequest.Builder(host, requestCode, perms)
                        .setRationale(rationale)
                        .build());
    }
Copy the code

Obviously, the internal requestPermissions() method is called, so continue to follow

    public static void requestPermissions(
            @NonNull Fragment host, @NonNull String rationale,
            int requestCode, @Size(min = 1) @NonNull String... perms) {
        requestPermissions(
                new PermissionRequest.Builder(host, requestCode, perms)
                        .setRationale(rationale)
                        .build());
    }
Copy the code

Builders Builder patterns created a PermissionRequest. Builder object, introduced to the real requestPermissions () method, with it

    public static void requestPermissions(PermissionRequest request) {

        // Check whether permissions are already included before requesting them
        if (hasPermissions(request.getHelper().getContext(), request.getPerms())) {
        	// The permission already exists, assign PERMISSION_GRANTED to the permission status array, and enter the request completion section. Instead of doing this process branch analysis, see for yourself
			notifyAlreadyHasPermissions(
                    request.getHelper().getHost(), request.getRequestCode(), request.getPerms());
            return;
        }

        // Use the helper class to call the system API to apply for permission
        request.getHelper().requestPermissions(
                request.getRationale(),
                request.getPositiveButtonText(),
                request.getNegativeButtonText(),
                request.getTheme(),
                request.getRequestCode(),
                request.getPerms());
    }
Copy the code

With requestPermissions () method

    public void requestPermissions(@NonNull String rationale,
                                   @NonNull String positiveButton,
                                   @NonNull String negativeButton,
                                   @StyleRes int theme,
                                   int requestCode,
                                   @NonNull String... perms) {
		/ / here traversing API call system, shouldShowRequestPermissionRationale, whether need to prompt the user application
		if (shouldShowRationale(perms)) {
            showRequestPermissionRationale(
                    rationale, positiveButton, negativeButton, theme, requestCode, perms);
        } else {
        	// Abstract methods that call system APIS in different subclasses
        	// ActivityCompat.requestPermissions(getHost(), perms, requestCode); methodsdirectRequestPermissions(requestCode, perms); }}Copy the code

At this point, the first request flow is complete, interacting with the user, as we demonstrated in the GIF above, to allow one permission and deny the other. At this time back to the Activity of the callback onRequestPermissionsResult method

	@Override
	public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
		super.onRequestPermissionsResult(requestCode, permissions, grantResults);

		// Pass the EasyPermissions class to handle the event
		EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
	}
Copy the code

With in!

    public static void onRequestPermissionsResult(int requestCode,
                                                  @NonNull String[] permissions,
                                                  @NonNull int[] grantResults,
                                                  @NonNull Object... receivers) {
        // Create two lists to collect the results of permission requests
        List<String> granted = new ArrayList<>();
        List<String> denied = new ArrayList<>();
        for (int i = 0; i < permissions.length; i++) {
            String perm = permissions[i];
            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                granted.add(perm);
            } else{ denied.add(perm); }}/ / traverse
        for (Object object : receivers) {
            // If a permission is granted, call back to the Activity's onPermissionsGranted method
            if(! granted.isEmpty()) {if (object instanceofPermissionCallbacks) { ((PermissionCallbacks) object).onPermissionsGranted(requestCode, granted); }}// If a permission is denied, call back to the onPermissionsDenied method in the Activity
            
            if(! denied.isEmpty()) {if (object instanceofPermissionCallbacks) { ((PermissionCallbacks) object).onPermissionsDenied(requestCode, denied); }}// If all requested permissions are granted, enter our annotated method @afterpermissionGranted. The use of annotations is not analyzed in detail here.
            if(! granted.isEmpty() && denied.isEmpty()) { runAnnotatedMethods(object, requestCode); }}}Copy the code

OnPermissionsGranted and onPermissionsDenied we are granting one permission and denying the other. This is processed in the onPermissionsDenied method in demo

    @Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());

        // (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
        // This will display a dialog directing them to enable the permission in app settings.
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show(); }}Copy the code

Made a judgment, ` EasyPermissions. SomePermissionPermanentlyDenied, callback to here is a list, let’s continue the analysis. Stay in there! Stay in there!

    public static boolean somePermissionPermanentlyDenied(@NonNull Activity host, @NonNull List
       
         deniedPermissions)
        {
        return PermissionHelper.newInstance(host)
                .somePermissionPermanentlyDenied(deniedPermissions);
    }
Copy the code

We enter the helper class again

    public boolean somePermissionPermanentlyDenied(@NonNull List<String> perms) {
        for (String deniedPermission : perms) {
            if (permissionPermanentlyDenied(deniedPermission)) {
                return true; }}return false;
    }
Copy the code

Each permission is iterated over. Return true if one of them is true. Continue to!

    public boolean permissionPermanentlyDenied(@NonNull String perms) {
    	/ / returns the shouldShowRequestPermissionRationale of value, the value is the system API shouldShowRequestPermissionRationale
        return! shouldShowRequestPermissionRationale(perms); }Copy the code

Here and do not have permission to filter out the user has agreed to, normal interaction will not enter the new AppSettingsDialog. Builder (this). The build (), show (); , but there is a problem with clicking cancel on the Rationale box. Let’s look at the Rationale box for the permission description.

From the demo application permissions requestPermissions method, called showRequestPermissionRationale method. Find the concrete implementation in the ActivityPermissionHelper class

@Override
    public void showRequestPermissionRationale(@NonNull String rationale,
                                               @NonNull String positiveButton,
                                               @NonNull String negativeButton,
                                               @StyleRes int theme,
                                               int requestCode,
                                               @NonNull String... perms) {
        FragmentManager fm = getHost().getFragmentManager();

        // Check if fragment is already showing
        Fragment fragment = fm.findFragmentByTag(RationaleDialogFragment.TAG);
        if (fragment instanceof RationaleDialogFragment) {
            Log.d(TAG, "Found existing fragment, not showing rationale.");
            return;
        }
		// Create a DialogFragment and display it
        RationaleDialogFragment
                .newInstance(positiveButton, negativeButton, rationale, theme, requestCode, perms)
                .showAllowingStateLoss(fm, RationaleDialogFragment.TAG);
    }
Copy the code

Look at the RationaleDialogFragment class, which doesn’t have much code, and find the implementation of the cancel button.

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Rationale dialog should not be cancelable
        setCancelable(false);

        / / create the listener
        RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
        RationaleDialogClickListener clickListener =
                new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);

        // Pass the listener into the dialog
        return config.createFrameworkDialog(getActivity(), clickListener);
    }
Copy the code

Check the RationaleDialogClickListener code

    @Override
    public void onClick(DialogInterface dialog, int which) {
        int requestCode = mConfig.requestCode;
        if (which == Dialog.BUTTON_POSITIVE) { // Click OK
            String[] permissions = mConfig.permissions;
            if(mRationaleCallbacks ! =null) {
                mRationaleCallbacks.onRationaleAccepted(requestCode);
            }
            if (mHost instanceof Fragment) {
                PermissionHelper.newInstance((Fragment) mHost).directRequestPermissions(requestCode, permissions);
            } else if (mHost instanceof Activity) {
                PermissionHelper.newInstance((Activity) mHost).directRequestPermissions(requestCode, permissions);
            } else {
                throw new RuntimeException("Host must be an Activity or Fragment!"); }}else { // Click Cancel
            if(mRationaleCallbacks ! =null) {
                mRationaleCallbacks.onRationaleDenied(requestCode);
            }
            // Call the following methodnotifyPermissionDenied(); }}private void notifyPermissionDenied(a) {
        if(mCallbacks ! =null) {
        	// This calls back the Activity's onPermissionsDenied() method, passing in two permissions
        	// When the user clicks reject, only one permission is passed to the user. Instead, both the permission and the permission are passed to the user.mCallbacks.onPermissionsDenied(mConfig.requestCode, Arrays.asList(mConfig.permissions)); }}Copy the code

Next in the execution somePermissionPermanentlyDenied () to determine, have been allowed permissions in internal call system APIshouldShowRequestPermissionRationale whether to return is false, In easyPermission, it is said that the user checked not to remind again, thus causing the problem.

So here’s the problem. What do we do about it? We can in onPermissionsDenied method for permissions do I already have a first screening, there will be no through the user agrees to access into somePermissionPermanentlyDenied, can solve the problem. Of course, you can also change the internal code, recompile and package it into the project.

Clever design in EasyPermissions

Now that the code has been analyzed, let’s move on to the clever design points in EasyPermissions. If you look closely at the code, you can see that the rationale pop box in the project is implemented as a DialogFragment, And AppsettingDialog is in AppSettingsDialogHolderActivity (an empty Activity) through internal finish the AlertDialog AppSettingsDialog class can be created and display (AppSettings Dialog is not a Dialog, just a helper class.

public class RationaleDialogFragmentCompat extends AppCompatDialogFragment {... }Copy the code
public class AppSettingsDialog implements Parcelable {... }Copy the code
public class AppSettingsDialogHolderActivity extends AppCompatActivity implements DialogInterface.OnClickListener {... }Copy the code

The actual dialog to the Settings is created in the AppSettingsDialog

    AlertDialog showDialog(DialogInterface.OnClickListener positiveListener, DialogInterface.OnClickListener negativeListener) {
        AlertDialog.Builder builder;
        if (mThemeResId > 0) {
            builder = new AlertDialog.Builder(mContext, mThemeResId);
        } else {
            builder = new AlertDialog.Builder(mContext);
        }
        return builder
                .setCancelable(false)
                .setTitle(mTitle)
                .setMessage(mRationale)
                .setPositiveButton(mPositiveButtonText, positiveListener)
                .setNegativeButton(mNegativeButtonText, negativeListener)
                .show();
    }
Copy the code

Why create a separate Activity to host the Dialog? The onActivityResult method in our own project can be unified. No matter clicking OK or cancel on the dialog set by the jump, the jump of the Activity will be involved and the onActivityResult () method will be called back. Perform uniform user grant or deny permission processing.

conclusion

Referring to Google Samples, I think the most user-friendly application process would be

  1. Users click a function button (for example, scan), apply for required permissions (camera permissions), and invoke the system pop-up box to interact with users.
  2. If the user refuses, the pop-up box prompts the user with the reason why the user needs permission. The user clicks “Agree” and invokes the system pop-up box again to apply for permission.
  3. The user refused again (already clicked no more reminder), prompting the user to use this function must obtain permission, guide the user to set the interface to manually open.

Pay attention to wechat public number, the latest technology dry goods real-time push