One article solves "Android permission problem (full version)"

I. Introduction

The content of the article is as follows:
If you just want to quickly complete your Android permission application, then go directly to the tool PermissionX.
If you want to really understand Android permission issues, then I suggest you read this article in 15 minutes. (You don’t need to experiment, save it for later use)
First, understand the relationship between the Android version and the SDK, and help us distinguish the later permission version.
Secondly, let's talk about the most common permission issues that we need to pay attention to in the Android7-13 version.
The last is the use of tools for dynamically applying for permissions that are now required by Apps.

2. The relationship between the Android version and the SDK version

At present, the latest Android version has reached Android13, and the corresponding SDK version has also reached 33. It’s better to look at the picture, just look at it directly.
Location: (Android Studio -> Preferences -> Appearance & Behavior -> System Settings -> Android SDK)

3. Changes in permissions in Android version changes

Let me talk about the versions that have changed significantly here, as well as the changes in the last few versions:
We all know that the permission changes from Android5.1 -> Android6.0 are well understood by everyone. But still, it won't do you any harm to look more.
You can see the permission change from Android5.1 to Android6.0, which corresponds to our SDK23 version.
That is to say, when the targetSdkVersion setting in build.gradle is less than 23, it will continue to refer to the old version permission management mechanism. When the targetSdkVersion is greater than or equal to 23 , the new permissions management will be used.

1, Android5 and earlier — targetSdkVersion < 23

When targetSdkVersion is less than 23, your project will continue to use the permission mechanism of the old version: the
requested permission only needs to be listed in AndroidManifest
insert image description here
. All permissions do not require pre-judgment when using permissions.
2. If the user manually goes to the settings to turn off the permission, our project will crash when using this permission, so if you use low version permission management, please try-catch the place where the permission is needed.
3. The prompt pop-up windows for low version permissions are all customized by mobile phone manufacturers. They have system permissions, and will prompt the user to authorize permissions when detecting your App usage permissions. If the user refuses, they will go to the settings center to close the permissions for the user.

2、Android6 — targetSdkVersion23

1. Permissions are divided into common permissions and privacy permissions. Common permissions are directly obtained if they are declared in the manifest file. Privacy permissions also need to be declared in the manifest file, but after the installation is complete, all privacy permissions are in a denied state, and it is necessary to determine whether the permissions are enabled when using privacy permissions, otherwise the project will directly crash.
For example, commonly used privacy permissions: camera, positioning, phone calls, text messages, read and write storage, microphone recording, sensors. (See the figure below for details)
privacy rights
There is a concept of permission group in the figure above. If any permission in the same group is authorized, other permissions will also be automatically authorized.
For example, once WRITE_EXTERNAL_STORAGE is authorized, APP also has READ_EXTERNAL_STORAGE permission.
The steps to apply for permission here must be clear to everyone, so I will just mention it casually. (Because you can directly use the following PermissionX to help you reduce these cumbersome permission processing processes)
1. Declare in AndroidManifest.xml
2. Check our unauthorized permissions checkSelfPermission(Context context, String permission)
3. Apply for our permissions requestPermissions( Activity activity, String[] permissions, int requestCode)
4. The result of applying for permissions onRequestPermissionsResult(int requestCode, String[] permissions, String[] grantResults)
5. The follow-up is to process the logic by yourself (PermissionX provides some processing solutions)

3、Android7 — targetSdkVersion24、25

In order to improve the security of the private directory and prevent the leakage of application information, starting from Android 7.0, the access permissions of the application private directory are restricted. Specifically, developers can no longer simply access other applications' private directory files through file:// URI or let other applications access their own private directory files. An error exception that triggers FileUriExposedException.
Solution: Use FileProvider (the specific use will not be repeated),
as one of the four major components, ContentProvider, which has always played the role of sharing resources between applications. The FileProvider we want to use here is a special subclass of ContentProvider, which helps us convert the file:// URI with limited access to the content:// URI that can authorize sharing.

4、Android8 — targetSdkVersion26、27

Prior to Android 8.0, if an app requested a permission at runtime and was granted that permission, the system would incorrectly grant the app along with other permissions that belonged to the same permission group and were registered in the manifest. (Permission groups are mentioned above)
This behavior has been corrected for apps targeting Android 8.0. The system only grants permissions that an app explicitly requests. However, once a user grants a permission to an app, all subsequent requests for permissions in that permission group will be automatically approved.
Everyone will know the problem, that is, the WRITE_EXTERNAL_STORAGE corresponding to the READ_EXTERNAL_STORAGE application will also be applied, but it is not possible now.
Solution : If you have not dynamically applied for WRITE_EXTERNAL_STORAGE before, you can add it to the application permission list and apply.
But here READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE is a permission group, so when you apply, you are only prompted to agree to a storage permission.

5、Android9 — targetSdkVersion28

The foreground service needs to add permissions
After Android 9.0, you must grant the FOREGROUND_SERVICE permission to use the foreground service, otherwise an exception will be thrown.

6、Android10 — targetSdkVersion29

Here comes the important point: Scoped storage (this is a good talk about it, because this is a problem that many applications will encounter).
This new feature directly subverts the way we have been using external storage space for a long time, so many apps are Will face the upgrade of more code modules.
Android has long supported the function of external storage space, which is what we often call SD card storage. Therefore, App likes to create its own exclusive directory under the root directory of the SD card to store various files and data.
Advantages : First, the files stored in the SD card will not be included in the space occupied by the application, that is to say, even if you store a 1G file in the SD card, the space occupied by your application displayed in the settings may still be only Dozens of K.
Second, the files stored on the SD card, even if the application is uninstalled, these files will still be retained, which helps to achieve some functions that require data to be permanently retained.
Disadvantages : First, rogue behavior Even if I uninstall a program that I no longer use at all, the junk files generated by it may remain on my phone. Second, the
files stored on the SD card are public files, and all All applications have the right to access at will, which also poses a great challenge to data security. (Google took action on this rogue behavior, that is, scoped storage)
What is the scope of Google:
starting from Android 10, each application can only have the right to read and store data in its own external storage space associated directory Create a file.
The code to get the directory is:

context.getExternalFilesDir()
// storage/emulated/0/Android/data/<包名>/files(大概目录为)

Store the data in this directory, and you will be able to use the previous writing method to read and write files without any changes or adaptations. But at the same time, the two "benefits" mentioned just now do not exist. That is to say, the storage related to the application deletion will also disappear.
Then some friends may ask, what should I do if I just need to access other directories? For example, read pictures in the mobile phone album, or add a picture to the mobile phone album.
To this end, the Android system classifies file types. Three types of files, pictures, audio, and video, can be accessed through the MediaStore API, while other types of files need to be accessed using the system's file selector.
The specific solution for adapting to Android 10 is written below: Android 10 Storage Adaptation Solution

7、Android11 — targetSdkVersion30

If it is adapted according to the Android10 solution, then nothing needs to be done.
If it is temporarily set requestLegacyExternalStorage="true"

<manifest ... >
  <application android:requestLegacyExternalStorage="true" ...>
    ...
  </application>
</manifest>

Then congratulations: Scoped Storage will be forcibly enabled. That is, the requestLegacyExternalStorage setting is invalid.
Solution :
1. Adapt according to the solution of Android 10 (it is best to adapt. Otherwise, subsequent versions will have to be continuously modified)
2. You must declare the MANAGE_EXTERNAL_STORAGE permission in AndroidManifest.xml

 <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
     tools:ignore="ScopedStorage" />

Note that compared to the traditional declaration of a permission, an attribute such as tools:ignore="ScopedStorage" is added here. Because if this attribute is not added, Android Studio will remind us with a warning that most applications should not apply for this permission.
Then use the action ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION to jump to the specified authorization page for authorization. (So ​​let's adapt to the Android10 solution)

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R ||
        Environment.isExternalStorageManager()) {
    
    
    Toast.makeText(this, "已获得访问所有文件权限", Toast.LENGTH_SHORT).show()
} else {
    
    
    val builder = AlertDialog.Builder(this)
        .setMessage("本程序需要您同意允许访问所有文件权限")
        .setPositiveButton("确定") {
    
     _, _ ->
            val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
            startActivity(intent)
        }
    builder.show()
}

8、Android12 — targetSdkVersion31、32

1. The fuzzy positioning function is added , and the user can choose to allow the application to only access the approximate location.
On Android 12, if your application needs to obtain the user's accurate location information, you need to apply for both the exact location and approximate location permissions. The code in the AndroidManifest.xml file is as follows:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

Note: These two permissions also need to be applied dynamically.
2. Obtain the installed application list permission.
On Android 11, when using the PackageManger method to obtain the installed application list, you need to apply for android.permission in the AndroidManifest.xml file. QUERY_ALL_PACKAGES this permission, but some mobile phones in Android 12 need to add android.permission.GET_INSTALLED_APPS permission to get the application list normally. The permission code is as follows:

    <uses-permission android:name="android.permission.GET_INSTALLED_APPS"/>
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />

Although android12 does not need to dynamically apply for these two permissions, a prompt for authorized access will appear when entering the application for the first time.
3. Bluetooth runtime permissions:
Added several Bluetooth-related runtime permissions. The reason is that when developers access some Bluetooth-related interfaces, they need to apply for geographic location permissions, which makes some privacy-sensitive users very disgusted. The following three oh.
BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT.

9、Android13 — targetSdkVersion33

1. Google has further refined the local data access permissions on Android 13.
Starting from Android 13, if your application targetSdk is specified to 33 or above, then the READ_EXTERNAL_STORAGE permission is completely useless, and applying for it will not generate any effect.
Correspondingly, Google has added three runtime permissions, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, and READ_MEDIA_AUDIO, which are used to manage photos, videos, and audio files of the mobile phone, respectively.
In other words, in the past, you only need to apply for a READ_EXTERNAL_STORAGE permission. It is no longer possible, you have to apply on demand, so that users can understand more precisely which media permissions your app has applied for.

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

2. Notification runtime permission
In the previous Android system, any application that wants to send a notification does not need the user's consent, and can send it if it wants to. This makes the notification bar of our mobile phone often occupied by some garbage notifications.
This time Android 13 incorporates notifications into runtime permission management, that is to say, if you want to send notifications in the future, you must first obtain the user's consent and authorization.

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

3. WIFI runtime permissions
Similar to Bluetooth, when developers want to access some WIFI-related interfaces, such as hotspots, WIFI direct connection, WIFI RTT, etc., they also need to apply for geographic location permissions.

A new NEARBY_WIFI_DEVICES permission has been added in Android 13. When using the WIFI API related to the above scenarios, we only need to apply for the NEARBY_WIFI_DEVICES permission, which better protects the user's privacy.

4. Android10 storage permission adaptation

1. Temporary solution (only suitable for adapting to Android10)

Temporary solutions are only suitable for solving problems on Android 10, and are not allowed in Android 11. That is, if your targetSdkVersion is upgraded to 30, it needs to be re-adapted.
The temporary solution for Android 10 is very simple, just add the following configuration to AndroidManifest.xml:

<manifest ... >
  <application android:requestLegacyExternalStorage="true" ...>
    ...
  </application>
</manifest>

This configuration means that even on the Android 10 system, it is still allowed to use the legacy external storage space to run the program, so that there is no need to make any changes to the code.

2. The first is scope storage (mentioned above, so I won’t go into details)

To put it simply, it is to replace the method you used to obtain the root directory of the SD card for random storage with your own external storage directory.
Original: Create the folder storage you want after obtaining it (Android10 and above are not allowed, that is, your targetSdkVersion >= 29)

Environment.getExternalStorageDirectory()
// storage/emulated/0 (正常获取的默认目录)

Modified to: own external storage space, which can be stored

context.getExternalFilesDir()
// storage/emulated/0/Android/data/<包名>/files (获取的默认目录)

3. Read phone photo album, video, audio

Here are only examples of pictures, video and audio are obtained in the same way.
Unlike the past, where we could directly obtain the absolute path of pictures in the album, in scoped storage, we can only use the MediaStore API to obtain the Uri of pictures. The sample code is as follows:

val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
if (cursor != null) {
    while (cursor.moveToNext()) {
        val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
        val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
        println("image uri is $uri")
    }
        cursor.close()
}

In the above code, we first obtained the ids of all the pictures in the album through ContentResolver, and then assembled the ids into a complete Uri object with the help of ContentUris. The Uri format of a picture is roughly as follows:

content://media/external/images/media/321

Then some friends may ask, after getting the Uri, how should I display this picture? There are many ways to do this, such as using Glide to load images, which itself supports passing in Uri objects as image paths:

Glide.with(context).load(uri).into(imageView)

4. Add pictures to album (video, audio)

Adding a picture to the mobile phone album is a little more complicated.
Directly add the code:
Step 1 : Build a ContentValues ​​object
Step 2 : Add its three parameters. One is the name displayed by the picture: DISPLAY_NAME. One is the mime type of the image: MIME_TYPE. There is also a path for image storage, but this value is handled differently in Android 10 and previous system versions. A new RELATIVE_PATH constant has been added in Android 10, indicating the relative path of file storage. The optional values ​​​​are DIRECTORY_DCIM, DIRECTORY_PICTURES, DIRECTORY_MOVIES, DIRECTORY_MUSIC, etc., which represent albums, pictures, movies, music and other directories. However, there is no RELATIVE_PATH in the previous system version, so we have to use the DATA constant (which has been discarded in Android 10) and assemble an absolute path for file storage.
Step 3 : Call the insert() method of ContentResolver to get the Uri of the inserted picture.
Step 4 : Call the openOutputStream() method of ContentResolver to obtain the output stream of the file, and then write the Bitmap object into the output stream.

fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) {
    val values = ContentValues()
    values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
    values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
    } else {
        values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName")
    }
    val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
    if (uri != null) {
        val outputStream = contentResolver.openOutputStream(uri)
        if (outputStream != null) {
            bitmap.compress(compressFormat, 100, outputStream)
                        outputStream.close()
        }
    }
}

5. Download the file to the Download directory

Performing file download operations is a very common scenario, such as downloading pdf, doc files, or downloading APK installation packages, etc. In the past, we usually downloaded these files to the Download directory, which is a directory specially used to store downloaded files. And starting from Android 10, we can no longer access the external storage space with an absolute path, so the file download function will also be affected.
The first solution : It is also the easiest way, which is to change the download directory of the file. Download the file to the associated directory of the application, so that the program can work normally on the Android 10 system without modifying any code. But using this method, you need to know that the downloaded file will be counted in the space occupied by the application, and if the application is uninstalled, the file will also be deleted together. In addition, the files stored in the associated directory can only be accessed by the current application program, and other programs have no read permission.
The second solution :
downloading the file to the Download directory is similar to the process of adding a picture to the album. Android 10 adds a new Downloads collection in MediaStore, which is specially used to perform file download operations.

val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)

The rest of your download process does not need to be changed, only the part you write to the file is replaced.

5. Direct access to the tool: dynamic application permission PermissionX

Having said so much, let's go directly to the tools, after all, work is important.
PermissionX1.7 fully supports Android 13's runtime permission application.
Normal permission application: 1. Check permission, apply for permission, return the result, process after return (jump to manual, etc.), the code is bloated, and the process is cumbersome. Is there, is there, is there.

Realization principle

Before the specific usage, let's briefly talk about its implementation principle,
and then developers gradually began to derive some encapsulation solutions, such as encapsulating the operation of runtime permissions into BaseActivity, or providing a transparent Activity to handle the operation time permissions, etc. (It is heavyweight when it comes to Activity, after all, it needs to be registered.)
At this time, someone thought of registering with a lightweight Fragment. After all, Android also provides the same API in Fragment, so that we can also use it in Fragment. Apply for runtime permissions.
But the difference is that Fragment does not have to have an interface like Activity. We can add a hidden Fragment to Activity, and then encapsulate the runtime permission API in this hidden Fragment. This is a very lightweight approach, so you don't have to worry about the impact of hiding the Fragment on the performance of the Activity.
This is the implementation principle of PermissionX.

use

1. Import tools

repositories {
    
    
  google()
  mavenCentral()
}

dependencies {
    
    
    implementation 'com.guolindev.permissionx:permissionx:1.7.1'
}

2. Basic and easy to use

PermissionX.init(this)
.permissions(Manifest.permission.CALL_PHONE)
.request {
    
     allGranted, grantedList, deniedList ->
    if (allGranted) {
    
    
        call()
    } else {
    
    
        Toast.makeText(this, "您拒绝了拨打电话权限", Toast.LENGTH_SHORT).show()
    }
}

Yes, the basic usage of PermissionX is as simple as that. First call the init() method to initialize, and pass in a FragmentActivity parameter during initialization. Since AppCompatActivity is a subclass of FragmentActivity, as long as your Activity inherits from AppCompatActivity, you can directly pass in this.
Next, call the permissions() method to pass in the permission name you want to apply for, here pass in the CALL_PHONE permission. You can also pass in any number of permission names in the permissions() method, separated by commas.
Finally, call the request() method to execute the permission application, and process the application result in the Lambda expression. It can be seen that there are three parameters in the Lambda expression: allGranted indicates whether all requested permissions have been granted, grantedList is used to record all authorized permissions, and deniedList is used to record all denied permissions.
Compared with the original method, it is much simpler and clearer! ! ! !

Handling of other situations

1. If the user refuses a certain permission, before the next application, we'd better pop up a dialog box to explain to the user the reason for applying for this permission. How can this be realized?
The onExplainRequestReason() method can be used to monitor permissions that are rejected by the user and can be applied for again.
And we only need to connect the onExplainRequestReason() method before the request() method, as shown below:

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, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    
    
        Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
}

In this case, all permissions denied by the user will first enter the onExplainRequestReason() method for processing, and the denied permissions are recorded in the deniedList parameter. Next, we only need to call the showRequestReasonDialog() method in this method, and a dialog box explaining the reason for the permission application will pop up

PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.onExplainRequestReason {
    
     deniedList ->
    showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
}
.request {
    
     allGranted, grantedList, deniedList ->
    if (allGranted) {
    
    
        Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    
    
        Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
}

The showRequestReasonDialog() method accepts 4 parameters: the first parameter is the permission list to be reapplied, and the deniedList parameter is directly passed in here. The second parameter is the reason to explain to the user. I just wrote a sentence casually. The more detailed the description of this parameter, the better. The third parameter is the text of the OK button on the dialog box, and the permission application operation will be re-executed after clicking the button. The fourth parameter is an optional parameter. If it is not passed, it means that the user must agree to the requested permissions, otherwise the dialog box cannot be closed. If it is passed in, there will be a cancel button on the dialog box. After clicking cancel, it will not Reapply for permissions, but call back the current application result to the request() method.
The specific function will give you an address to see for yourself: the master leads the door to practice in the individual

Guess you like

Origin blog.csdn.net/weixin_45112340/article/details/128905213