Good morning, everyone. Where have you read line 3 of Code?

Some of my friends are really impressive in their reading speed. I remember that in less than a week after The 3rd Line of Code was released, someone had already read chapter 9 (because of the hidden keywords in Chapter 9 in the background of the wechat account). Now, “the third line of code” has been published more than a month, I believe that many friends will have read the whole book.

Those of you who have read the book know that the last chapter of Line 3 takes you through the development of an open source library: PermissionX. The purpose of this chapter is to give you an overview of the development and distribution process of an open source library. To better illustrate this process, I came up with the idea of writing a library like PermissionX.

However, the overall functionality of the PermissionX library is relatively simple, as the focus of this chapter is not on how to make the open source library perfect and powerful, but rather on the development and distribution process.

But later on, I felt that PermissionX could be a real library for simplifying The handling of Android runtime permissions, not just for educational purposes, but for practical projects to help people deal with the pain points of handling runtime permissions.

As a result, PermissionX is now ready to be released. PermissionX is now available.

The open source library is at github.com/guolindev/P…

Where are the pain points?

No one wants to write code that deals with Android runtime permissions because it’s just too cumbersome.

It’s a low-tech job that you have to do because otherwise the program will crash. It would be nice if it were easier to handle, but the fact is that the Runtime permissions API that Android gives us is not friendly.

Taking a call function as an example, the CALL_PHONE permission is a dangerous permission, so in addition to declaring the permission in androidmanifest.xml, we need to perform runtime permission processing before performing the call operation.

The permission statement is as follows:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissionx.app">

    <uses-permission android: />
	...
	
</manifest>

Copy the code

Then, write the following code for runtime permission handling:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        makeCallBtn.setOnClickListener {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
                call()
            } else {
                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call()
                } else {
                    Toast.makeText(this, "You denied CALL_PHONE permission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun call() {
        try {
            val intent = Intent(Intent.ACTION_CALL)
            intent.data = Uri.parse("tel:10086")
            startActivity(intent)
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }

}

Copy the code

The real functional logic in this code is in the call() method, but if you call the call() method directly, you won’t be able to make a call because you haven’t applied for the CALL_PHONE permission yet.

Then the rest of the code is dealing with the CALL_PHONE permission request. Here you can see, the need to determine whether the user has authorization we call first, if not have access to application, then the onRequestPermissionsResult () callback of the permissions application processing results, the ability to execute the call operation.

And you might say, well, that’s not too much, that’s not a lot of code. That’s because so far we’ve only dealt with the simplest scenarios for run-time permissions, and there are more complex scenarios waiting for us in the real project environment.

For example, your App may not only apply for one permission, but for multiple permissions at the same time. Although ActivityCompat. RequestPermissions () method allows a one-time pass more permissions, but you’re onRequestPermissionsResult () callback will need to determine which permissions are allowed, in which permissions are rejected, Whether the denied permission affects the core functionality of the application, and whether you need to apply for permission again.

When it comes to applying for permission again, a more complicated issue arises. If you have applied for permission that has been rejected by the user once, there is a high chance that it will be rejected again. To this end, Android provides a shouldShowRequestPermissionRationale () method, is used to determine whether the cause of the need to explain to the user to apply for this permission, Once shouldShowRequestPermissionRationale () method returns true, then we’d better pop up a dialog to the user to clarify why we are need the permissions, so can increase the risk of users to authorize.

Is it complicated already? But that’s not all. Android also offers a “refuse, don’t ask” option, as shown below:

As long as the user selects this option, our code for each subsequent permission request will be rejected.

But what if I have to rely on this permission for a feature? There is no way, you can only prompt the user to go to the application Settings to manually open permissions, procedures have been unable to operate.

As you can see, it can be quite complicated to have a very comprehensive handle on runtime permissions in a project. In fact, most projects don’t handle permission requests properly, which is why I wrote PermissionX.

How PermissionX works

Before we dive into the specific uses of PermissionX, let’s discuss how it works.

It’s not that no one has tried to encapsulate runtime permission processing before, and I showed you a runtime PERMISSION API encapsulating process in a live public lecture.

But, want to encapsulate the runtime permissions API is not an easy thing, because this operation is a specific context dependent, general need in the Activity receives onRequestPermissionsResult () method of the callback, So you can’t simply encapsulate the entire operation into a separate class.

For this purpose, a series of special encapsulation schemes have been developed, such as encapsulating runtime permission operations into BaseActivity, or providing a transparent Activity to handle runtime permissions.

Neither option is lightweight enough, however, because changing an Activity’s inheritance structure is a big deal, and providing a transparent Activty requires additional declarations in androidmanifest.xml.

Now, there’s another trick that’s generally accepted in the industry. What’s the trick? Recall that in the past, all requests for runtime permissions were done in the Activity. In fact, Android provides the same API in the Fragment that allows you to apply for runtime permissions in the Fragment.

However, unlike an Activity, a Fragment does not have to have an interface. It is perfectly possible to add a hidden Fragment to an Activity and then encapsulate the runtime permissions API in that hidden Fragment. This is a very lightweight way to do this without worrying about how hiding fragments will affect the performance of your Activity.

This is how PermissionX works, and it’s already covered in the book. However, based on the implementation principle, I have added many new features to make PermissionX more powerful and useful. Let’s learn how to use PermissionX in detail.

Basic usage

Before you can use PermissionX, you first need to introduce it into your project, as shown below:

dependencies { ... Implementation 'com. Permissionx. Guolindev: permissionx: 1.1.1'}Copy the code

The latest version of PermissionX as I write this article is 1.1.1. To view the current version, visit the home page of PermissionX: github.com/guolindev/P…

The goal of PermissionX is to make runtime permission handling as easy as possible, so making the API easy to use is one of my top priorities.

For example, to make a phone call, use PermissionX and write:

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) makeCallBtn.setOnClickListener { PermissionX.init(this) .permissions(Manifest.permission.CALL_PHONE) .request { allGranted, grantedList, DeniedList -> if (allGranted) {call()} else {toast.maketext (this, "You denied call permission ", Toast.LENGTH_SHORT).show() } } } } ... }Copy the code

Yes, the basic usage of PermissionX is that simple. The init() method is first called to initialize, passing in a FragmentActivity parameter at initialization. Since AppCompatActivity is a subclass of FragmentActivity, you can just pass this as long as your Activity inherits from AppCompatActivity.

Next, call the permissions() method to pass in the name of the permissions you want to apply for, in this case the CALL_PHONE permission. You can also pass in any number of permission names in the permissions() method, separated by commas.

Finally, the request() method is called to perform the permission request and the result of the request is processed in a Lambda expression. As you can see, the Lambda expression has three parameters: allGranted to indicate whether all requested permissions have been granted, grantedList to record allGranted permissions, and deniedList to record all denied permissions.

If allGranted is true, call(), otherwise a Toast prompt will pop up.

The running results are as follows:

How’s that? Does it feel like runtime permissions are a little less cumbersome than they were before?

The core usage

However, we have only dealt with the most common scenario so far. As mentioned above, if the user rejects a permission, before the next application, we should pop up a dialog box to explain the reason for applying for the permission. How should this be implemented?

Don’t worry, PermissionX takes full account of these cases.

The onExplainRequestReason() method can be used to listen for permissions that have been rejected by the user and can be applied for again. As you can see from the method name, the reason for requesting these permissions should be explained in this method.

Instead, we simply string the onExplainRequestReason() method before the request() method, as follows:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE) .onExplainRequestReason { deniedList -> } .request { allGranted, grantedList, DeniedList -> if (allGranted) {toast. makeText(this, "all permissions approved ", Toast.length_short).show()} else {toast.maketext (this, "you rejected the following permission: $deniedList", toast.length_short).show()}}Copy the code

In this case, all denied permissions are processed in the onExplainRequestReason() method first, and denied permissions are recorded in the deniedList parameter. Next, we simply call the showRequestReasonDialog() method in this method to pop up a dialog explaining the reason for the permission request, as shown below:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE) .onExplainRequestReason { deniedList -> showRequestReasonDialog(deniedList, Request {allGranted, grantedList, grantedList)}. Request {allGranted, grantedList, DeniedList -> if (allGranted) {toast. makeText(this, "all permissions approved ", Toast.length_short).show()} else {toast.maketext (this, "you rejected the following permission: $deniedList", toast.length_short).show()}}Copy the code

The showRequestReasonDialog() method takes four arguments: the first argument is the list of permissions to reapply for, where the deniedList argument is passed directly. The second parameter is the reason to explain to the user, I just wrote a sentence, this parameter description is as detailed as possible. The third parameter is the text of the “Ok” button on the dialog box. Clicking this button will re-execute the permission application operation. The fourth parameter is an optional parameter, if it is not the equivalent of a user must agree to apply for these permissions, or dialog box cannot be shut down, and if the incoming, there will be a cancel button on dialog box, click cancel after not to apply permissions, but the result of the current application callback to request () method.

Also, always remember to declare all permissions in androidmanifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.permissionx.app">

    <uses-permission android: />
    <uses-permission android: />
    <uses-permission android: />
	...
	
</manifest>

Copy the code

Run the program again, and the result should look like the picture below:

The “explain permission request reason” dialog cannot be customized in the current version. In version 1.3.0, the “explain permission request reason” dialog can be customized. For details, see PermissionX’s major update.

Of course, we can also specify which permissions to apply for again. For example, among the three permissions mentioned above, I think CAMERA permission is essential, while the other two permissions are dispensable, so we can only apply for CAMERA permission when applying again:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION) .onExplainRequestReason { deniedList -> val filteredList = deniedList.filter { It = = Manifest. Permission. CAMERA} showRequestReasonDialog (filteredList, "CAMERA permissions are procedures must rely on the authority", "I knew," Request {allGranted, grantedList, deniedList -> if (allGranted) {toa. makeText(this, "all privileges granted ", Toast.length_short).show()} else {toast.maketext (this, "you rejected the following permission: $deniedList", toast.length_short).show()}}Copy the code

When requesting permissions again, only CAMERA permissions will be requested, and the remaining two permissions will eventually be passed to the deniedList argument in the request() method.

Now that we’ve solved the problem of explaining permissions to users, we have another headache to solve: what if the user still refuses permission despite our explanation, and chooses to reject and never ask again? In this case, the application level cannot request permission again, the only thing to do is to prompt the user to manually open the permission in the application Settings.

So how does PermissionX handle this situation? I’m sure you’ll be pleasantly surprised. PermissionX also provides an onForwardToSettings() method that listens for permissions that have been permanently rejected by the user. In addition, as the method name indicates, we can remind the user to manually open permissions in the application Settings. The code looks like this:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE) .onExplainRequestReason { deniedList -> showRequestReasonDialog(deniedList, "About to apply for the permission to access the program must rely on", "I understand", "cancel")}. OnForwardToSettings {deniedList - > showForwardToSettingsDialog (deniedList, Request {allGranted, grantedList, grantedList)}. Request {allGranted, grantedList, DeniedList -> if (allGranted) {toast. makeText(this, "all permissions approved ", Toast.length_short).show()} else {toast.maketext (this, "you rejected the following permission: $deniedList", toast.length_short).show()}}Copy the code

As you can see, the onForwardToSettings() method is used to process all permissions that the user has chosen to reject and no longer asks for. The denied permissions are recorded in the deniedList parameter.

Next, you don’t need to bring up a Toast or dialog to remind users to open access of application Settings manually, but direct call showForwardToSettingsDialog () method. Similarly, showForwardToSettingsDialog () method also receive four parameters, the effect of each parameter and just showRequestReasonDialog () method is the same, I will not repeat explained here.

ShowForwardToSettingsDialog () method will pop up a dialog box, when users click on the I have learnt the buttons on the dialog box, will automatically jump to the current application Settings interface, thus don’t need the user to enter the Settings for the current application of the. In addition, when the user returns from the Settings, PermissionX will automatically rerequest the corresponding permissions and call back the final authorization results to the Request () method. The effect is shown below:

Also, version 1.3.0 also supports the ability to customize this dialog style. For details, see the PermissionX blockbuster update to support custom permission notification dialog boxes.

More usage

These are probably the main features of PermissionX, although I’ve found that some apps like to pop up a dialog explaining what permissions they need before requesting them for the first time. This is desirable because the user is more likely to agree to authorization.

So how does this work in PermissionX?

Actually very simple, PermissionX also provides a explainReasonBeforeRequest () method, just need to it also concatenate the request () method is ok, before the code is as follows:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE) .explainReasonBeforeRequest() .onExplainRequestReason { deniedList -> ShowRequestReasonDialog (deniedList, "The permissions to be requested are permissions that the program must depend on ", "I understand")}. OnForwardToSettings {deniedList - > showForwardToSettingsDialog (deniedList, "you need to manually open access of application Settings". Request {allGranted, grantedList, deniedList -> if (allGranted) {toasts. MakeText (this, "all privileges granted ", Toast.length_short).show()} else {toast.maketext (this, "you rejected the following permission: $deniedList", toast.length_short).show()}}Copy the code

In this way, onExplainRequestReason() will be given priority every time a request is made, and a dialog box will pop up explaining why the request was made, and the user will only execute the request after clicking the “I understand” button. The effect is shown below:

However, you’re using explainReasonBeforeRequest () method, there are some key points to note.

First, single use explainReasonBeforeRequest () method is invalid, must cooperate with onExplainRequestReason () method is used to work together. This makes sense, since the onExplainRequestReason() method is not configured, how do we explain the permission request reason to the user?

Second, in the use of explainReasonBeforeRequest () method, if onExplainRequestReason () method to write permissions filtering logic, the final results may be expected with you. This point may be a little hard to understand, but let me illustrate it with a concrete example.

Observe the following code:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE) .explainReasonBeforeRequest() .onExplainRequestReason { deniedList -> val filteredList = DeniedList. Filter {it = = Manifest. Permission. CAMERA} showRequestReasonDialog (filteredList, "CAMERA permissions are procedures must rely on the authority". "I see ")}...Copy the code

Here we write the permission filtering logic we just used in the onExplainRequestReason() method. When multiple permissions are denied, we only re-apply for CAMERA permissions.

Without adding explainReasonBeforeRequest () method, everything can be normal operation as we expected. But if added explainReasonBeforeRequest () method, the execute permissions request before entering onExplainRequestReason () method, which will be in addition to the CAMERA’s other rights are filtered out, Therefore, PermissionX will actually request only one CAMERA permission, and the rest of the permission will not be attempted at all, but will be called back to the final Request () method as denied permission.

The effect is shown below:

In this case, PermissionX provides an additional beforeRequest parameter in the onExplainRequestReason() method that identifies whether the current context is before or after the permission request, This problem can be solved nicely by executing different logic in the onExplainRequestReason() method with this parameter, as shown in the following code:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE) .explainReasonBeforeRequest() .onExplainRequestReason { deniedList, BeforeRequest -> if (beforeRequest) {showRequestReasonDialog(deniedList) Please agree to the following permission application ", "I understand")} else {val filteredList = deniedList. Filter {it = = Manifest. Permission. CAMERA} ShowRequestReasonDialog (filteredList)}}...Copy the code

As you can see, when beforeRequest is true, the permission request has not yet been performed, so we pass the complete deniedList to the showRequestReasonDialog() method.

When beforeRequest is false, it means that some permissions have been rejected by users. At this time, we only re-apply for CAMERA permissions, because it is essential, and other permissions are optional.

The final running effect is as follows:

Permission-Support

The name of this library is PermissionX, so needless to say, it’s definitely compatible with AndroidX. In case some of you are not sure what AndroidX is, here is a popular science article I wrote before that I always hear people say AndroidX, what is AndroidX?

However, I’m sure there are still a lot of projects that aren’t using AndroidX and are still using the old Android Support Library. For this reason, I’ve provided a version of the Android Support Library: Permission-support.

In terms of usage, there is no difference between the two versions, and everything discussed above in this article applies to Permission-support. Only when referencing libraries, if you are prepared to use permission-support, use the following dependency library addresses:

dependencies { ... Implementation 'com. Permissionx. Guolindev: permission - support: 1.1.1'}Copy the code

However, the Android Support Library is destined to be completely phased out by Google in the near future, so I won’t be maintaining permission-support for long, just for a while. PermissionX is something I intend to maintain for a long time and will continue to add new and useful features.

Afterword.

Finally, there are some friends who want to ask, can Java language projects use PermissionX?

Since version 1.2.2, PermissionX has added support for the Java language. However, PermissionX has changed its usage slightly to make it compatible with Java syntax. See this article for details. PermissionX now supports Java! Android 11 permission changes are also explained.

The new library has just been released, there may be a lot of bugs THAT I have not been able to detect by myself, please help to test more, and make the library more perfect together.

Once again post the open source library address for PermissionX and welcome to Star and fork.

Github.com/guolindev/P…

Also, if you want to learn the Kotlin language or the latest Android knowledge like Android 10 and Jetpack, you can read my new book, The First Line of Code — Android Version 3.

The previous article is now on the Nuggets.

Pay attention to my technical public account “Guo Lin”, there are high-quality technical articles pushed every week.