[android 11, android 10 key change adaptation]

android 11 key changes

One: partition storage

1: It has been changed to mandatory, android:requestLegacyExternalStorage="true" method is invalid

Recommended reading article: Android 11 new features, Scoped Storage has new tricks

android 10 key changes

official document

One: partition storage

internal storage

1 file

You used to be like this:
the path corresponding to the Environment.getExternalStorageDirectory() associated directory is roughly as follows:

/storage/emulated/0

Now you are officially required to do this: mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
The path corresponding to the associated directory is roughly as follows:

/storage/emulated/0/Android/data/<包名>/files/Pictures

Cache type files can be like this: val externalCacheDirPath = externalCacheDir!!.absolutePath

The path corresponding to the associated directory is roughly as follows

/storage/emulated/0/Android/data/<包名>/cache

You are smart, you must be able to see the files we are storing now, all under the package name. Let it take its course, you may think that after uninstalling the app, the files we stored are also deleted. The answer is yes. What are the advantages of doing this? In this blog of Guo Shen , it is better, you can go and have a look.

Environment has the following properties:

  • DIRECTORY_PICTURES
  • DIRECTORY_DOWNLOADS
  • DIRECTORY_MOVIES
  • DIRECTORY_AUDIOBOOKS
  • DIRECTORY_MUSIC
  • etc.

The adaptation code is roughly as follows:

 //返回通用图片储存路径,统一在这个方法中,做android 10 的适配
    public static String getCommonSavePath(Context mContext) {
        String path = "";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {//适配android 10
            path = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + "";
        } else {
            path = Environment.getExternalStorageDirectory().getAbsolutePath();
        }
        return path;
    }

2 media

Get the adaptation of the picture in the album

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)
    }
	cursor.close()
}

Adaptation for adding pictures to albums

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

remove picture from album

    private fun deleteImageFromAlbum() {
    
    
        val imageFileName = "20201130ypk6667.jpg"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    

            val queryPathKey = MediaStore.MediaColumns.DISPLAY_NAME;
            val cursor = contentResolver.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                null,
                "$queryPathKey =? ",
                arrayOf(imageFileName),
                null
            )
            if (cursor != null) {
    
    
                Log.e("ypkTest", "cursor is ");
                while (cursor.moveToNext()) {
    
    
                    val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
                    val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
                    println("ypkTest.deleteFil11e uri=${
      
      uri.toString()}")
                    deleteDealWith(uri);
                }
            } else {
    
    
                Log.e("ypkTest", "cursor is null");
            }

           /* val where = MediaStore.Images.Media.DISPLAY_NAME + "='" + imageFileName + "'"
            //测试发现,只有是自己应用插入的图片,才可以删除。其他应用的Uri,无法删除。卸载app后,再去删除图片,此方法不会抛出SecurityException异常
            val result = contentResolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, where, null)
            Log.i("ypkTest", "deleteImageFromAlbum1 result=${result}");
            if (result > 0) {
                Toast.makeText(this, "delete sucess", Toast.LENGTH_LONG).show()
            }*/

        } else {
    
    
            val filePath = "${
      
      Environment.getExternalStorageDirectory().path}/${
      
      Environment.DIRECTORY_DCIM}/$imageFileName";
            val where = MediaStore.Images.Media.DATA + "='" + filePath + "'"
            val result = contentResolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, where, null)
            Log.i("ypkTest", "result=${
      
      result}");
            if (result > 0) {
    
    
                Toast.makeText(this, "delete sucess", Toast.LENGTH_LONG).show()
            }

        }


    }

    /**
     * 知识补充:
     * 开了沙箱之后,之前的媒体库生成的文件在其记录上会打上owner_package的标志,标记这条记录是你的app生成的。
     * 当你的app卸载后,MediaStore就会将之前的记录去除owner_package标志,
     * 也就是说app卸载后你之前创建的那个文件与你的app无关了(不能证明是你的app创建的)。
     * 所以当你再次安装app去操作之前的文件时,媒体库会认为这条数据不是你这个新app生成的,所以无权删除或更改。
     * 处理方案:
     * 采用此种方法,删除相册图片,会抛出SecurityException异常,捕获后做下面的处理,会出现系统弹框,提示你是否授权删除。
     * 点击授权后,我们在onActivityResult回调中,再次做删除处理,理论上就能删除。
     *
     * 测试发现:小米8,Android10,是有系统弹框提示,提示是否授权,授权后在去删除,删除的result结果也是1,
     * 根据result的值判断,确实是删除了。但是相册中,依然存在。不知道为何是这样?
     *
     * 参考文章:https://blog.csdn.net/flycatdeng/article/details/105586961
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    private fun deleteDealWith(uri: Uri) {
    
    
        try {
    
    
            val result = contentResolver.delete(uri, null, null)
            println("ypkTest.deleteImageFromDownLoad result=$result")
            if (result > 0) {
    
    
                Toast.makeText(this, "delete  succeeded.", Toast.LENGTH_SHORT).show()
            }
        } catch (securityException: SecurityException) {
    
    
            Log.e("ypkTest", "securityException=${
      
      securityException.message}");
            securityException.printStackTrace()

            val recoverableSecurityException =
                securityException as? RecoverableSecurityException
                    ?: throw securityException
            // 我们可以使用IntentSender向用户发起授权
            val intentSender =
                recoverableSecurityException.userAction.actionIntent.intentSender
            startIntentSenderForResult(
                intentSender,
                REQUEST_DELETE_PERMISSION,
                null,
                0,
                0,
                0,
                null
            )
        }
    }

Video and audio files are basically the same, in summary:

The MediaStore API provides interfaces to access the following types of media files:

  • Photos: stored in MediaStore.Images.
  • Video: Stored in MediaStore.Video.
  • Audio files: stored in MediaStore.Audio.

Here is a summary of the places where most applications need to be modified:
(1) Selection of local photos, storage path when saving pictures to local
(2) When processing media files in external storage
(3) Storage when app upgrades and downloads apk to local path

3 Description

Generally, we use the getFilesDir() or getCacheDir() method to obtain the internal storage path of this application. To read and write files under this path, there is no need to apply for storage space read and write permissions, and it will be automatically deleted when the application is uninstalled. The path storage using this method does not need to be adapted!
The corresponding path is roughly as follows:

filesDir.absolutePath

   /data/user/0/app的包名/files

cacheDir.absolutePath

   /data/user/0/app的包名/cache

external storage

Addition, deletion, modification and query of the Download directory in the public area

Here we will focus on adding, deleting, modifying and checking the Download directory, and the use of contentResolver.

increase

   val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image)
        val displayName = "${System.currentTimeMillis()}.jpg"
        val compressFormat = Bitmap.CompressFormat.JPEG


        val values = ContentValues()
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/pactera/com/TestFile3/")
        val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
        println("MainActivity.downloadFile1=${uri.toString()}") //content://media/external/downloads/1362855
        println("MainActivity.downloadFile2=${uri!!.path}")  ///external/downloads/1362855


        if (uri != null) {
            val outputStream = contentResolver.openOutputStream(uri)
            if (outputStream != null) {
                bitmap.compress(compressFormat, 100, outputStream)
                outputStream.close()
                Toast.makeText(this, "Add bitmap to album succeeded.", Toast.LENGTH_SHORT).show()
            }
        }

check

Query all:

 val cursor = contentResolver.query(
                MediaStore.Downloads.EXTERNAL_CONTENT_URI,
                null,
                null,
                null,
                null
            )
            if (cursor != null) {
                println("MainActivity.deleteFil11e cursor is")

                while (cursor.moveToNext()) {
                    val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
                    val name = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
                    val relative_path = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//相对路径

                    //根据图片id获取uri,这里的操作是拼接uri
                    val uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)

                    println("MainActivity.deleteFil11e id=$id")
                    println("MainActivity.deleteFil11e name=$name")
                    println("MainActivity.deleteFil11e relative_path=$relative_path")
                    println("MainActivity.deleteFil11e uri=${uri.toString()}")
                    
                }
                cursor.close()
            } else {
                println("MainActivity.deleteFil11e cursor is null")
            }

Search by file name:

           val fileName = "1604584554910.jpg"; 
           val queryPathKey = MediaStore.MediaColumns.DISPLAY_NAME;
            val cursor = contentResolver.query(
                MediaStore.Downloads.EXTERNAL_CONTENT_URI,
                null,
                queryPathKey + " =? ",
                arrayOf(fileName),
                null
            )
            if (cursor != null) {
                println("MainActivity.deleteFil11e cursor is")

                while (cursor.moveToNext()) {
                      val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
                    val name = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
                    val relative_path = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//相对路径

                    //根据图片id获取uri,这里的操作是拼接uri
                    val uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)

                    println("MainActivity.deleteFil11e id=$id")
                    println("MainActivity.deleteFil11e name=$name")
                    println("MainActivity.deleteFil11e relative_path=$relative_path")
                    println("MainActivity.deleteFil11e uri=${uri.toString()}")

                }
                cursor.close()
            } else {
                println("MainActivity.deleteFil11e cursor is null")
            }

Check according to relative path:

     
          val filePath = Environment.DIRECTORY_DOWNLOADS + "/pactera/com/TestFile/"
           val queryPathKey = MediaStore.MediaColumns.RELATIVE_PATH;
            val cursor = contentResolver.query(
                MediaStore.Downloads.EXTERNAL_CONTENT_URI,
                null,
                queryPathKey + " =? ",
                arrayOf(filePath ),
                null
            )
            if (cursor != null) {
                println("MainActivity.deleteFil11e cursor is")

                while (cursor.moveToNext()) {
                      val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
                    val name = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
                    val relative_path = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//相对路径

                    //根据图片id获取uri,这里的操作是拼接uri
                    val uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)

                    println("MainActivity.deleteFil11e id=$id")
                    println("MainActivity.deleteFil11e name=$name")
                    println("MainActivity.deleteFil11e relative_path=$relative_path")
                    println("MainActivity.deleteFil11e uri=${uri.toString()}")

                }
                cursor.close()
            } else {
                println("MainActivity.deleteFil11e cursor is null")
            }

ps: Pay attention to the file path filePath here, and / must be added at the end, otherwise the data cannot be found.

Search according to the file name and relative path:

   val fileName = "1604584554910.jpg"; 
   val filePath = Environment.DIRECTORY_DOWNLOADS + "/pactera/com/TestFile/"
   
   val queryPathKey = MediaStore.MediaColumns.DISPLAY_NAME;
            val queryPathKey2 = MediaStore.MediaColumns.RELATIVE_PATH;
            val cursor = contentResolver.query(
                MediaStore.Downloads.EXTERNAL_CONTENT_URI,
                null,
                queryPathKey + " =? and " + queryPathKey2 + " =?",
                arrayOf(fileName, filePath),
                null
            )
            if (cursor != null) {
                println("MainActivity.deleteFil11e cursor is")

                while (cursor.moveToNext()) {
                    val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
                    val name =
                        cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
                    val relative_path =
                        cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//图片名字

                    val uri =
                        ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
                    println("MainActivity.deleteFil11e id=$id")
                    println("MainActivity.deleteFil11e name=$name")
                    println("MainActivity.deleteFil11e relative_path=$relative_path")
                    println("MainActivity.deleteFil11e uri=${uri.toString()}")

                

                }
                cursor.close()
            } else {
                println("MainActivity.deleteFil11e cursor is null")
            }

The important thing is said three times: the test found that if the file was not created by you or the file was moved or copied, there will be a problem that the file cannot be found, and of course the program will not report an error. In other words: only the files you created can be found, updated, and deleted.
The important thing is said three times: the test found that if the file was not created by you or the file was moved or copied, there will be a problem that the file cannot be found, and of course the program will not report an error. In other words: only the files you created can be found, updated, and deleted.
The important thing is said three times: the test found that if the file was not created by you or the file was moved or copied, there will be a problem that the file cannot be found, and of course the program will not report an error. In other words: only the files you created can be found, updated, and deleted.

change

  while (cursor.moveToNext()) {
                    val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
                    val name =
                        cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
                    val relative_path =
                        cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//图片名字

                    val uri =
                        ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
                    println("MainActivity.deleteFil11e id=$id")
                    println("MainActivity.deleteFil11e name=$name")
                    println("MainActivity.deleteFil11e relative_path=$relative_path")
                    println("MainActivity.deleteFil11e uri=${uri.toString()}")

                    val values = ContentValues()
                    values.put(MediaStore.MediaColumns.DISPLAY_NAME, "1604584554910ypk.jpg")
                    var result = contentResolver.update(uri, values, null, null)
                    println("MainActivity.deleteFil11e result=" + result)

              

                }

Description: In this way, the found file can be renamed to 1604584554910ypk.jpg

delete

 while (cursor.moveToNext()) {
                    val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
                    val name =
                        cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
                    val relative_path =
                        cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//图片名字

                    val uri =
                        ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
                    println("MainActivity.deleteFil11e id=$id")
                    println("MainActivity.deleteFil11e name=$name")
                    println("MainActivity.deleteFil11e relative_path=$relative_path")
                    println("MainActivity.deleteFil11e uri=${uri.toString()}")

          
                      if (fileName  == name) {
                       var result= contentResolver.delete(uri, null, null)
                        println("MainActivity.deleteFil11e result="+result)
                    }

                }

Article source code

Reference blog: Recommended: Android 10 adaptation key points, scoped storage

Two: Enhanced user control over location permissions

Foreground-only permissions that give users more control over app access to device location information

Affected Apps: Apps that request access to the user's location while in the background

Note: If you should use the positioning function, you must do adaptation processing. In order to allow users to better control the application's access to location information, Android 10 introduces the ACCESS_BACKGROUND_LOCATION permission. Unlike the ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions, the ACCESS_BACKGROUND_LOCATION permission only affects the app's access to location information while it is running in the background. For more instructions visit here .

How to adapt it? Don't worry, let's take our time. In Android 10, not only the ACCESS_COARSE_LOCATION permission must be dynamically applied, but the ACCESS_BACKGROUND_LOCATION permission must also be dynamically applied together! Only requesting the ACCESS_BACKGROUND_LOCATION permission has no effect!

Here I will share a pitfall I encountered when I was doing the adaptation. I hope everyone will not make the same mistake. I integrated the Gaode map. Go directly to the code:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                val selfPermission4 = ContextCompat.checkSelfPermission(
                    activity,
                    Manifest.permission.ACCESS_BACKGROUND_LOCATION
                )
                //如果请求此权限,则还必须请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION权限。只请求此权限无效果。
                if (selfPermission4 != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(
                        activity,
                        arrayOf(
                            Manifest.permission.ACCESS_FINE_LOCATION,
                            Manifest.permission.ACCESS_COARSE_LOCATION,
                            Manifest.permission.ACCESS_BACKGROUND_LOCATION
                        ),
                        BACKGROUND_LOCATION_REQUESTCODE
                    )
                }
  }

Note: It is strongly recommended that these three permissions are applied together, otherwise there will be very strange problems.

Three: The system executes the background Activity

Implemented restrictions on launching activities from the background

Affected Apps: Apps that start an Activity without user interaction

Four: non-resettable hardware identifier

Restrictions are enforced on accessing device serial number and IMEI

Apps affected: Apps that access the device serial number or IMEI

Five: Wireless scanning permissions

Access to certain Wi-Fi, Wi-Fi Sense, and Bluetooth scanning methods requires precise location permission

Affected Apps: Apps using Wi-Fi API and Bluetooth API


Reference article:

Android 10 Adaptation Guide

The basic method and framework use of android 6.0 dynamic application permission

Basic usage:

First provide the official learning documents of goog:

Permissions Best Practices

Ask for permissions at runtime

 int selfPermission = ContextCompat.checkSelfPermission(Main2Activity.this, Manifest.permission.CALL_PHONE);
        if (selfPermission != PackageManager.PERMISSION_GRANTED) {

            /**
             * 判断该权限请求是否已经被 Denied(拒绝)过。  返回:true 说明被拒绝过 ; false 说明没有拒绝过
             *
             * 注意:
             * 如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了 Don't ask again 选项,此方法将返回 false。
             * 如果设备规范禁止应用具有该权限,此方法也会返回 false。
             */
            if (ActivityCompat.shouldShowRequestPermissionRationale(Main2Activity.this, Manifest.permission.CALL_PHONE)) {
                Log.i(TAG, "onViewClicked: 该权限请求已经被 Denied(拒绝)过。");
                //弹出对话框,告诉用户申请此权限的理由,然后再次请求该权限。
                //ActivityCompat.requestPermissions(Main2Activity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);

            } else {
                Log.i(TAG, "onViewClicked: 该权限请未被denied过");

                ActivityCompat.requestPermissions(Main2Activity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
            }

        } else {
            openAlbum();//打开相册
        }

The callback that initiates the request:

 @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        Log.i(TAG, "onRequestPermissionsResult: requestCode=" + requestCode);

        switch (requestCode) {
            case 1:
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    openAlbum();
                } else {
                    Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show();
                    Log.i(TAG, "onRequestPermissionsResult: you denied the permission");
                }
                break;

            default:
                break;


        }


    }

Encapsulation of some frameworks:

Simple use of HiPermission: For more information, see related articles in the reference blog.

This method applies for almost all three permissions necessary. camera, location, sd card write

 //CAMERA, ACCESS_FINE_LOCATION and WRITE_EXTERNAL_STORAGE
        HiPermission.create(this)
                .animStyle(R.style.PermissionAnimModal)
                //.style(R.style.PermissionDefaultGreenStyle)
                .checkMutiPermission(new PermissionCallback() {
                    @Override
                    public void onClose() {
                        Log.i(TAG, "onClose They cancelled our request"); //用户关闭权限申请
                    }

                    @Override
                    public void onFinish() {
                        Log.i(TAG, "onFinish: All permissions requested completed");  //所有权限申请完成
                    }

                    @Override
                    public void onDeny(String permission, int position) {
                        Log.i(TAG, "onDeny");//在否认
                    }

                    @Override
                    public void onGuarantee(String permission, int position) {
                        Log.i(TAG, "onGuarantee");//用户允许后,会回调该函数  //在此可以做 事件处理啦,因为用户已经同意了,此时已经拿到所需权限啦。
                    }
                });

Since 6.0, what permissions need to be obtained dynamically? List it here:
It is divided into 9 groups. As long as one permission application in each group is successful, the whole group of permissions can be used by default.

group:android.permission-group.CONTACTS
    permission:android.permission.WRITE_CONTACTS
    permission:android.permission.GET_ACCOUNTS    
    permission:android.permission.READ_CONTACTS
  
  group:android.permission-group.PHONE
    permission:android.permission.READ_CALL_LOG
    permission:android.permission.READ_PHONE_STATE 
    permission:android.permission.CALL_PHONE (打电话)
    permission:android.permission.WRITE_CALL_LOG
    permission:android.permission.USE_SIP
    permission:android.permission.PROCESS_OUTGOING_CALLS
    permission:com.android.voicemail.permission.ADD_VOICEMAIL
  
  group:android.permission-group.CALENDAR
    permission:android.permission.READ_CALENDAR
    permission:android.permission.WRITE_CALENDAR
  
  group:android.permission-group.CAMERA 
    permission:android.permission.CAMERA ( 相机 )
  
  group:android.permission-group.SENSORS
    permission:android.permission.BODY_SENSORS
  
  group:android.permission-group.LOCATION (位置相关)
    permission:android.permission.ACCESS_FINE_LOCATION
    permission:android.permission.ACCESS_COARSE_LOCATION
  
  group:android.permission-group.STORAGE ( SD卡读写权限 )
    permission:android.permission.READ_EXTERNAL_STORAGE
    permission:android.permission.WRITE_EXTERNAL_STORAGE
  
  group:android.permission-group.MICROPHONE
    permission:android.permission.RECORD_AUDIO
  
  group:android.permission-group.SMS (短信相关)
    permission:android.permission.READ_SMS  (读取短信)
    permission:android.permission.RECEIVE_WAP_PUSH
    permission:android.permission.RECEIVE_MMS
    permission:android.permission.RECEIVE_SMS (接受短信)
    permission:android.permission.SEND_SMS  (发送短信)
    permission:android.permission.READ_CELL_BROADCASTS

Google official document address (need to overturn the wall): https://developer.android.com/guide/topics/security/permissions.html
Write picture description here


Reference blog:

RxPermissions has more than 8k stars until 2019/2/23

The star is more than 5K, and it has been updated,
easypermissions is good

One line of code to get the beautiful Android6.0 permission application interface
HiPermission

This attention is relatively high: as of 2019/2/23, there are more than 8.6K stars, and it supports kotlin and can also be used as a backup
PermissionsDispatcher

Guess you like

Origin blog.csdn.net/da_caoyuan/article/details/109486444