This article is simultaneously published on my wechat public account. You can follow it by searching Guo Lin on wechat. The article is updated every weekday.

Hello everyone, today we have an original article. We are happy to announce that PermissionX has a new version.

I’ve been working on GDG for a long time, so I’ve put a lot of work on hold. As soon as GDG was over, I went back to work and released the new version of PermissionX as quickly as possible.

As you can see from the frequency with which I update this project, this is not just a project I’m writing about, but an open source project that I’m really going to maintain for the long term. If you find any problems in the process of use, you can also feedback to me.

There have been three iterations of PermissionX so far, with the latest version 1.3.0 adding the important ability to customize permission notification dialogs. If you thought the PermissionX permission notification dialog was too ugly to be used in a formal production environment, this time you can use your UI power to create a nice permission notification interface.

If you are not familiar with the use of PermissionX, you can check out my two previous posts on Android Runtime Permissions: PermissionX and PermissionX now support Java. Android 11 permission changes are also explained

Let’s take a look at what’s new in version 1.3.0.

The correct way to write background location permission

In the previous release, PermissionX introduced support for Android 11 permission changes. To better protect user privacy, in Android 11, ACCESS_BACKGROUND_LOCATION is a separate permission to apply for, and will crash if you apply for it with foreground location.

However, in Android 10, foreground and background positioning rights can be applied together. Although separate application is also possible, but the user experience is poor, because you need to pop the permission application dialog box twice.

In order to reduce this type of adaptation, PermissionX specifically ADAPTS permissions for Android 11. There is no need to write a set of permission processing logic for Android 10 and Android 11. Instead, it will be handed over to PermissionX, which will automatically handle the logic for each system version.

However, I have found that in practice, some developers have not been able to figure out the proper use of Android 11 permission adaptation and have asked me some questions. So before introducing new version 1.3.0, LET me ask you to demonstrate the correct way to apply for background location permissions.

Let’s start with what the question is, which I’ve been asked more than once.

PermissionX gets background location permissions in 8.0, which goes directly to the deniedList, or reject list.

Why does this happen? Since ACCESS_BACKGROUND_LOCATION is a new permission introduced in Android 10, it is not available in Android 8.0.

API Level 29 stands for Android 10.

The system does not have ACCESS_BACKGROUND_LOCATION, but I applied for ACCESS_BACKGROUND_LOCATION, so it was natural for me to be on the reject list.

Although it is a natural thing, the result of such a request can cause some friends trouble to use. Let’s look at the following code:

PermissionX.init(this) .permissions(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION) .request { allGranted, grantedList, DeniedList -> if (allGranted) {toast. makeText(activity, "All permissions approved ", Toast.length_short).show()} else {toast.maketext (activity, "You rejected the following permissions: $deniedList", toast.length_short).show()}}Copy the code

Here we have requested both ACCESS_FINE_LOCATION and ACCESS_BACKGROUND_LOCATION. If the system is running on Android 10 or above, as long as the user grants us both foreground location and background location permissions, AllGranted will be true for the final callback. On Android 10 or below, however, ACCESS_BACKGROUND_LOCATION is never granted, so allGranted must be false.

This is a bug that should automatically grant access to ACCESS_BACKGROUND_LOCATION in Android versions below 10. This is because the background location feature is already available in versions lower than Android 10.

I’ve been thinking about this for a long time. Should ACCESS_BACKGROUND_LOCATION be in the authorized list or denied list when running lower than Android 10?

I ended up sticking with the existing logic for the simple reason that if you call the system’s API to determine whether ACCESS_BACKGROUND_LOCATION permissions are granted on lower than Android 10, the answer is no. Therefore, it is more important for me to keep the results consistent with the system API, because PermissionX is essentially a wrapper around the system’s permissions API, and I should not tamper with the permissions returned by the system.

However, if you apply for both foreground and background permissions, what about the logic processing in different system versions? For lower than Android 10, allGranted must be false.

This isn’t a hard problem to solve, but let’s take a look at whether Android Studio thinks it’s completely correct.

When applying for ACCESS_BACKGROUND_LOCATION, Android Studio gives a warning that the API we called was added at Level 29 (Android 10.0). The current project is compatible with the lowest system version 15 (Android 4.0).

In other words, this is not a proper way to apply for permissions, because in earlier versions of mobile systems, the system cannot recognize what ACCESS_BACKGROUND_LOCATION is.

Therefore, it is most appropriate that we should not apply for ACCESS_BACKGROUND_LOCATION when our application is running on a phone below Android 10. Rather than wondering why ACCESS_BACKGROUND_LOCATION returns an unauthorized result.

Let me modify the above code:

val permissionList = ArrayList<String>() permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION) if (Build.VERSION.SDK_INT >= 29) { permissionList.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } PermissionX.init(this) .permissions(permissionList) .request { allGranted, grantedList, DeniedList -> if (allGranted) {toast. makeText(activity, "All permissions approved ", Toast.length_short).show()} else {toast.maketext (activity, "You rejected the following permissions: $deniedList", toast.length_short).show()}}Copy the code

As you can see, I’m adding the permissions to a List, but ACCESS_BACKGROUND_LOCATION will only be added to the List if the system version is greater than or equal to 29. Therefore, in the lower version of the mobile phone system, is not to apply for background location permission. This way, the value of the allGranted variable is no longer affected.

In addition, Android Studio will no longer warn us when using this notation.

Support the fragments

Fragments are now more common than activities, and the last version of PermissionX only supported passing in an instance of an Activity type when initialized.

Several friends have asked me how to apply for permissions in fragments using PermissionX. This question, to be honest, knocked me senseless, as if I hadn’t really thought about it before.

But then I thought, isn’t it possible to get an instance of an Activity in a Fragment? Just pass it to PermissionX after getActivity().

I think this works, but using PermissionX in fragments can cause an IllegalStateException based on some of the feedback we’ve gotten so far.

Since more than one person has encountered this problem, I think it may not be an accidental phenomenon.

Strangely enough, I tried every means to reproduce the problem myself, but I still couldn’t, whether it was due to the Fragment version used.

But that’s okay, if I can’t reproduce the problem, that doesn’t stop me from solving it. According to stackOverflow, when we add another sub-fragment to our Fragment, You should use ChildFragmentManager instead of FragmentManager.

Obviously, if you use getActivity(), PermissionX will still be using FragmentManager internally to add a hidden Fragment that requests permissions, and I think that’s what’s causing the problem.

The 1.3.0 version of PermissionX includes native support for fragments. Instead of calling getActivity() when using PermissionX in a Fragment, we can pass this directly, as shown in the following example:

class MainFragment : Fragment() { ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) PermissionX.init(this) .permissions(Manifest.permission.ACCESS_FINE_LOCATION) .request { allGranted,  grantedList, deniedList -> } } }Copy the code

Internally, PermissionX automatically determines that if the developer initializes an Activity and passes it in, the FragmentManager will be used automatically. If the Fragment is passed in, the ChildFragmentManager is automatically used. Part of the source code implementation is as follows:

private InvisibleFragment getInvisibleFragment() {
    FragmentManager fragmentManager;
    if (fragment != null) {
        fragmentManager = fragment.getChildFragmentManager();
    } else {
        fragmentManager = activity.getSupportFragmentManager();
    }
    ...
}

Copy the code

Of course, this is just a solution I deduced from the limited error information and solutions on StackOverflow. I can’t verify it on my own, because I haven’t been able to reproduce the problem myself.

If you still have this problem after using PermissionX 1.3.0, please continue to let me know and hopefully give me some guidance on how to reproduce this problem.

Custom permission reminder dialog box

The custom permission notification dialog is probably the most important feature of the 1.3.0 release.

PermissionX was very thoughtful about the permission process. For example, what if we were denied permission? What if our access is permanently denied? However, PermissionX’s notification dialog when permission is denied is the system’s default style and can only enter text content, which is not enough for many developers. As shown in the figure below.

Not being able to customize the interface you want is probably the biggest factor limiting PermissionX to a formal production environment.

Version 1.3.0 has completely solved this problem, and now you can customize the various dialogs to match your project’S UI style.

As for the usage of this part, it is very simple. PermissionX version 1.3.0 provides a RationalDialog abstract class that you can inherit from when you need to customize the permission alert dialog. RationaleDialog actually inherits the system’s Dialog class, so the use of custom dialogs is no different from what you would normally write.

It’s just that the purpose of our dialog is to explain to the user why we need to apply for these permissions, and let the user understand why and then agree to apply. Therefore, the dialog box must have an OK button above it, as well as an optional cancel button (not if you have to grant permissions). In addition, we need to know which permissions we are applying for, otherwise we don’t know what message to display on the screen.

Therefore, the RationaleDialog class defines three abstraction methods that you must implement when customizing dialog boxes, as follows:

public abstract class RationaleDialog extends Dialog {


    
    abstract public @NonNull View getPositiveButton();

    
    abstract public @Nullable View getNegativeButton();

    
    abstract public @NonNull List<String> getPermissionsToRequest();

}

Copy the code

The getPositiveButton() method returns the OK button on the current custom dialog; The getNegativeButton() method returns the cancel button on the current custom dialog, or null if the dialog cannot be cancelled. The getPermissionsToRequest() method returns which permissions are to be applied for.

RationaleDialog only forces you to implement these three methods, and the rest of the custom interface is up to you to implement however you want.

Now, when permission is denied, all we need to do is pass our custom dialog to the showRequestReasonDialog() method as follows:

val myRationaleDialog = ...
scope.showRequestReasonDialog(myRationaleDialog)

Copy the code

All set!

The task of PermissionX is very simple, but the most difficult part is customizing the UI. Therefore, the following I will demonstrate a custom dialog box implementation method for your reference.

A nice custom pair box interface needs to be divided into many steps to complete, here I show you step by step. The first step is to define a theme, edit the styles.xml file, and add the following:

<resources> ... <style > <! - the background color and transparent degree - > < item > @ android: color/transparent < item > <! <item >true</item> <! <item >@null</item> <! --> <item >true</item> <! - whether fuzzy - > < item > true < / item > < / style > < / resources >Copy the code

Next we will provide the background styles for the dialog box, as well as the background styles for the OK button and cancel button. Create the custom_dialog_bg. XML, positive_BUTTon_Bg. XML, and negative_button_Bg. XML files in the drawable directory, as shown below.

custom_dialog_bg.xml:

<? The XML version = "1.0" encoding = "utf-8"? > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#272727" /> <corners android:radius="20dp" /> </shape>Copy the code

positive_button_bg.xml

<? The XML version = "1.0" encoding = "utf-8"? > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#2084c2" /> <corners android:radius="20dp" /> </shape>Copy the code

negative_button_bg.xml

<? The XML version = "1.0" encoding = "utf-8"? > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#7d7d7d" /> <corners android:radius="20dp" /> </shape>Copy the code

Then create a new custom_dialog_layout. XML file under the layout directory, which is used as the layout of our custom dialog box. The code is as follows:

<? The XML version = "1.0" encoding = "utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/custom_dialog_bg" android:orientation="vertical" > <TextView android:id="@+id/messageText" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20sp" android:textColor="#fff" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="20dp" /> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_margin="20dp" android:scrollbars="none" android:layout_weight="1"> <LinearLayout android:id="@+id/permissionsLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> </ScrollView> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginBottom="10dp"> <Button android:id="@+id/negativeBtn" android:layout_width="120dp" android:layout_height="46dp" android:background="@drawable/negative_button_bg" <Button android:id="@+id/positiveBtn" Android :layout_width="120dp" android:layout_height="46dp" android:layout_marginStart="30dp" android:layout_marginLeft="30dp" Android :background="@drawable/positive_button_bg" Android :textColor="# FFF "Android :text=" open "/> </LinearLayout> </LinearLayout>Copy the code

A very simple interface that I won’t explain here.

In addition, since we are dynamically displaying which permissions to apply for in the dialog, we need to define an additional layout to display dynamic content. Create a new permissions_item. XML file in the Layout directory as follows:

<? The XML version = "1.0" encoding = "utf-8"? > <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/bodyItem" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" android:textSize="16sp" android:textColor="#fff"> </TextView>Copy the code

Dynamic content doesn’t have to be complicated, just use a TextView.

Ok, with the layout files defined, we can start coding. Create a new CustomDialog class that inherits from RationaleDialog and write the following code:

@TargetApi(30) class CustomDialog(context: Context, val message: String, val permissions: List<String>) : RationaleDialog(context, R.style.CustomDialog) { private val permissionMap = mapOf(Manifest.permission.READ_CALENDAR to Manifest.permission_group.CALENDAR, Manifest.permission.WRITE_CALENDAR to Manifest.permission_group.CALENDAR, Manifest.permission.READ_CALL_LOG to Manifest.permission_group.CALL_LOG, Manifest.permission.WRITE_CALL_LOG to Manifest.permission_group.CALL_LOG, Manifest.permission.PROCESS_OUTGOING_CALLS to Manifest.permission_group.CALL_LOG, Manifest.permission.CAMERA to Manifest.permission_group.CAMERA, Manifest.permission.READ_CONTACTS to Manifest.permission_group.CONTACTS, Manifest.permission.WRITE_CONTACTS to Manifest.permission_group.CONTACTS, Manifest.permission.GET_ACCOUNTS to Manifest.permission_group.CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION to Manifest.permission_group.LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION to Manifest.permission_group.LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION to Manifest.permission_group.LOCATION, Manifest.permission.RECORD_AUDIO to Manifest.permission_group.MICROPHONE, Manifest.permission.READ_PHONE_STATE to Manifest.permission_group.PHONE, Manifest.permission.READ_PHONE_NUMBERS to Manifest.permission_group.PHONE, Manifest.permission.CALL_PHONE to Manifest.permission_group.PHONE, Manifest.permission.ANSWER_PHONE_CALLS to Manifest.permission_group.PHONE, Manifest.permission.ADD_VOICEMAIL to Manifest.permission_group.PHONE, Manifest.permission.USE_SIP to Manifest.permission_group.PHONE, Manifest.permission.ACCEPT_HANDOVER to Manifest.permission_group.PHONE, Manifest.permission.BODY_SENSORS to Manifest.permission_group.SENSORS, Manifest.permission.ACTIVITY_RECOGNITION to Manifest.permission_group.ACTIVITY_RECOGNITION, Manifest.permission.SEND_SMS to Manifest.permission_group.SMS, Manifest.permission.RECEIVE_SMS to Manifest.permission_group.SMS, Manifest.permission.READ_SMS to Manifest.permission_group.SMS, Manifest.permission.RECEIVE_WAP_PUSH to Manifest.permission_group.SMS, Manifest.permission.RECEIVE_MMS to Manifest.permission_group.SMS, Manifest.permission.READ_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE, Manifest.permission.ACCESS_MEDIA_LOCATION to Manifest.permission_group.STORAGE ) private val groupSet = HashSet<String>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.custom_dialog_layout) messageText.text = message buildPermissionsLayout() window? . Let {val param = it. The attributes val width = (context) resources. The displayMetrics. WidthPixels * 0.8), toInt () val height = param.height it.setLayout(width, height) } } override fun getNegativeButton(): View? { return negativeBtn } override fun getPositiveButton(): View { return positiveBtn } override fun getPermissionsToRequest(): List<String> { return permissions; } private fun buildPermissionsLayout() { for (permission in permissions) { val permissionGroup = permissionMap[permission] if (permissionGroup ! = null && ! groupSet.contains(permissionGroup)) { val textView = LayoutInflater.from(context).inflate(R.layout.permissions_item, permissionsLayout, false) as TextView textView.text = context.packageManager.getPermissionGroupInfo(permissionGroup, 0).loadLabel(context.packageManager) permissionsLayout.addView(textView) groupSet.add(permissionGroup) } } } }Copy the code

This code doesn’t have a lot of space in the custom interface section. It just shows the layout we just customized using setContentView() in the onCreate() method.

But the permissionMap part of the code takes up a lot of space. Why write this code? Let me explain it to you.

The Android permissions mechanism is actually composed of permissions and permissions groups. A single permission group can contain multiple permissions, such as the READ_CALENDAR and WRITE_CALENDAR permissions in the CALENDAR permission group.

Usually, when applying for permission limits, we need to use the permission name instead of the permission group name. However, when one permission in the permission group is authorized, other permissions in the same group will also be automatically authorized, so there is no need to apply one by one.

Therefore, when we receive a list of permissions to apply for, we do not need to display all the permissions in the list on the interface, but only show the name of the permission group to apply for, which makes the interface more streamlined. By my previous count, Android 10 had 30 runtime permissions and only 11 permission groups.

The permissionMap and buildPermissionsLayout() methods in the above code actually handle this logic, fetching the corresponding permission group based on the permissions passed in and dynamically adding it to the dialog.

In addition, getPositiveButton(), getNegativeButton(), getPermissionsToRequest() are the most basic implementation of the three methods, the dialog box in the ok button, cancel button, and the list of permissions to apply.

That completes the custom permission alert dialog! The next thing to do is to use it, which is very simple. The code looks like this:

PermissionX.init(this) .permissions(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.RECORD_AUDIO) .onExplainRequestReason { scope, deniedList, BeforeRequest -> val dialog = CustomDialog(context, message, deniedList) scope.showRequestReasonDialog(dialog) } .onForwardToSettings { scope, DeniedList -> val dialog = CustomDialog(context, message, deniedList) scope.showForwardToSettingsDialog(dialog) } .request { allGranted, grantedList, DeniedList -> if (allGranted) {toast. makeText(activity, "All permissions approved ", Toast.length_short).show()} else {toast.maketext (activity, "You rejected the following permissions: $deniedList", toast.length_short).show()}}Copy the code

Most of the usage is no different from the previous version of PermissionX, which I won’t explain in detail. The most interesting point is in the Lambda expressions of the onExplainRequestReason and onForwardToSettings methods, where we create an instance of the CustomDialog, Then call the scope respectively. ShowRequestReasonDialog () and the scope. The showForwardToSettingsDialog () method, and the CustomDialog instance into the can.

And you’re done! Now run the application and you will experience a great permission request process, as shown below.

Of course, this is just a fairly basic custom permission notification dialog that I implemented, and now it’s time to make the most of your UI.

The complete code implementation of the above custom dialog box, WHICH I have put into the Open source project of PermissionX, can be downloaded and run directly to see the effect of the above picture.

How to upgrade

PermissionX < span style = “box-sizing: border-box; color: RGB (51, 51, 51); line-height: 22px; font-size: 14px! Important; word-break: inherit! Important;”

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

If your project has not been upgraded to AndroidX, you can use the same version of permission-support.

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

Overall let me comment, custom permission alert dialog box brings more possibilities, but in terms of ease of use, there are still some deficiencies, because custom a dialog box is generally more troublesome. So, in the next release, I’m going to build in some dialog box implementations to make PermissionX easier for those of you who don’t have a very demanding interface. Read on for details about how requesting permissions on Android can be such a great user experience.

If you want to learn about Kotlin and the latest on Android, check out my new book, Line 1, Version 3. Click here for more details.

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