【android 11 ,android 10关键变更适配】

android 11 关键变更

一:分区存储

1:已改为强制,android:requestLegacyExternalStorage=“true” 方法已失效

推荐阅读文章:Android 11新特性,Scoped Storage又有了新花样

android 10 关键变更

官方文档

一:分区存储

内部储存

1 文件

以前你是这样:Environment.getExternalStorageDirectory()
关联目录对应的路径大致如下:

/storage/emulated/0

现在官方要求你这样:mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
关联目录对应的路径大致如下:

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

缓存类型文件可这样:val externalCacheDirPath = externalCacheDir!!.absolutePath

关联目录对应的路径大致如下

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

聪明的你,一定能看到我们现在储存的文件,都在包名下啦。顺其自然你可能会想到,卸载app后我们存储的文件,是不是也都删除了呢。答案是肯定的。这样做有什么优点,这篇郭神的博客 中,讲的比较好大家可以去看看。

Environment 有以下属性:

  • DIRECTORY_PICTURES
  • DIRECTORY_DOWNLOADS
  • DIRECTORY_MOVIES
  • DIRECTORY_AUDIOBOOKS
  • DIRECTORY_MUSIC
  • 等等

适配代码大概如下:

 //返回通用图片储存路径,统一在这个方法中,做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 媒体

获取相册中的图片的适配

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

将图片添加到相册的适配

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

将图片从相册中删除

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

视频和音频文件基本上是同理的,总结:

MediaStore API 提供访问以下类型的媒体文件的接口:

  • 照片:存储在 MediaStore.Images 中。
  • 视频:存储在 MediaStore.Video 中。
  • 音频文件:存储在 MediaStore.Audio 中。

这里总结一下大部分应用都要修改的地方:
(1)本地照片的选择,保存图片到本地时储存路径
(2)处理外部存储中的媒体文件时
(3)app升级下载apk到本地时的储存路径

3 说明

一般我们使用getFilesDir() 或 getCacheDir() 方法获取本应用的内部储存路径,读写该路径下的文件不需要申请储存空间读写权限,且卸载应用时会自动删除。使用该方法进行的路径储存是不需要做适配的!
对应的路径大概如下:

filesDir.absolutePath

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

cacheDir.absolutePath

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

外部储存

公共区域Download目录的增删改查

这里着重讲解一下Download目录的增删改查,contentResolver 的使用。

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

查询全部:

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

根据文件名字查:

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

根据相对路径查:

     
          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: 注意这里的文件路径 filePath ,末尾要加 / ,不然查不到数据。

根据 文件名字 和 相对路径 查:

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

重要的事说三遍:测试发现,如果文件不是你创建的 或者 文件是移动,复制过去的,文件会出现查不到的问题,当然程序不会报错。换言之:只有是你创建的文件,你才能查到,更新,删除。
重要的事说三遍:测试发现,如果文件不是你创建的 或者 文件是移动,复制过去的,文件会出现查不到的问题,当然程序不会报错。换言之:只有是你创建的文件,你才能查到,更新,删除。
重要的事说三遍:测试发现,如果文件不是你创建的 或者 文件是移动,复制过去的,文件会出现查不到的问题,当然程序不会报错。换言之:只有是你创建的文件,你才能查到,更新,删除。

  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)

              

                }

说明:这样就能把查到的文件,重新命名为1604584554910ypk.jpg

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

                }

文章源码

参考博客:推荐:Android 10适配要点,作用域存储

二: 增强了用户对位置权限的控制力

仅限前台权限,可让用户更好地控制应用对设备位置信息的访问权限

受影响的应用:在后台时请求访问用户位置信息的应用

说明:如果你的应该用到了定位功能,那就必须做适配处理。为了让用户更好地控制应用对位置信息的访问权限,Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限。与 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 权限不同,ACCESS_BACKGROUND_LOCATION 权限仅会影响应用在后台运行时对位置信息的访问权限。更多说明请访问这里

那如何去适配呢?不急,我们慢慢说来。在Android10中不仅要动态申请ACCESS_COARSE_LOCATION权限,ACCESS_BACKGROUND_LOCATION权限也要 一起 动态申请哦!只请求ACCESS_BACKGROUND_LOCATION权限是没有效果的!

这里我分享一下我在做适配时,遇到的一个坑,希望大家不要犯同样的错误,我集成的是高德地图。直接上代码:

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

说明:强烈建议,这三个权限一起申请了,不然会出现很奇葩的问题。

三:系统执行后台 Activity

针对从后台启动 Activity 实施了限制

受影响的应用:不需要用户互动就启动 Activity 的应用

四:不可重置的硬件标识符

针对访问设备序列号和 IMEI 实施了限制

受影响的应用:访问设备序列号或 IMEI 的应用

五:无线扫描权限

访问某些 WLAN、WLAN 感知和蓝牙扫描方法需要获得精确位置权限

受影响的应用:使用 WLAN API 和蓝牙 API 的应用


参考文章:

Android 10 适配攻略

android 6.0 动态申请权限的基本方法和框架使用

基本的使用:

先提供一下goog官方的学习文档:

权限最佳做法

在运行时请求权限

 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();//打开相册
        }

发起请求的回调:

 @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;


        }


    }

一些框架的封装:

HiPermission 的简单使用:更多查看参考博客相关文章。

该方法 申请几乎必要的三个权限。照相,定位,sd卡写

 //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");//用户允许后,会回调该函数  //在此可以做 事件处理啦,因为用户已经同意了,此时已经拿到所需权限啦。
                    }
                });

自6.0以后,都有哪些权限需要动态获取呢?在此罗列一下:
共分为9组,每组只要有一个权限申请成功了,就默认整组权限都可以使用了。

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官方文档地址(需要翻墙):https://developer.android.com/guide/topics/security/permissions.html
这里写图片描述


参考博客:

RxPermissions 到2019/2/23为止star 8k多

star 5K多,而且一直在更新,不错
easypermissions

一行代码搞定漂亮的Android6.0权限申请界面
HiPermission

这个关注度比较高:到2019/2/23为止,star 8.6K多,并且支持了kotlin 也可作为备用
PermissionsDispatcher

猜你喜欢

转载自blog.csdn.net/da_caoyuan/article/details/109486444