版本相关-Android11

Android 11版本特性

Android 11 新特性,Scoped Storage又有了新花样

Android 11上强制启用了 Scoped Storage

需要适配的地方:关注Android 11的权限变更,这部分内容在 PermissionX现在支持Java了!还有Android 11权限变更讲解 这篇有比较详细的详解。

下文是关于 Scoped Storage 的探讨

Scoped Storage不是Android 11上新推的功能,在Android 10上就有,可以参考 Android 10适配要点,作用域存储 ,之前这篇并没有过时在Android 11上依然适用。

1
2
3
4
5
<manifest ... >
<application android:requestLegacyExternalStorage="true" ...>
...
</application>
</manifest>

这个配置在Android 11上依然有效,前提是 targetSdkVersion 必须小于等于 29

在 targetSdkVersion 等于 30 时强制启用了 Scoped Storage(上述配置不起效)

强制启用 Scoped Storage 后应用程序按照 Android 10适配要点适配就可以了。

但有一类应用程序非常特殊,即文件浏览器,如Root Explorer、ES Explorer等。就会有影响,无法以文件的真实路径来对文件进行管理。针对这类应用的解决方案:

管理设备上所有的文件

大部分应用程序来说,使用 MediaStore API 就可以满足开发需求了。以下是针对文件浏览器应用的。

文件浏览器需要对设备的整个SD卡进行管理,这类危险程度比较高的权限,Google通常采用的做法是,使用Intent跳转到一个专门的授权页面,引导用户手动授权,比如悬浮窗,无障碍服务等。

在Android 11上想要管理整个设备的文件也是使用类似的技术。做法:

  1. AndroidManifest.xml中声明MANAGE_EXTERNAL_STORAGE权限

    1
    2
    3
    4
    5
    6
    7
    8
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.scopedstoragedemo">

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

    </manifest>

    多了tools:ignore=”ScopedStorage”,不加这个的话 Android Studio会用一个警告提醒我们不应该申请这个权限。

  2. 使用ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION这个action来跳转到指定的授权页面,可以通过Environment.isExternalStorageManager()这个函数来判断用户是否已授权

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R ||
    Environment.isExternalStorageManager()) {//低于Android 11或已经拥有管理整个SD卡的权限,即可直接使用传统的写法,以文件真实路径的形式对文件进行操作
    Toast.makeText(this, "已获得访问所有文件权限", Toast.LENGTH_SHORT).show()
    } else {
    //没有管理SD卡的权限。弹出一个对话框,告知用户申请权限的原因,然后使用Intent跳转到指定的授权页面,让用户手动授权
    val builder = AlertDialog.Builder(this)
    .setMessage("本程序需要您同意允许访问所有文件权限")
    .setPositiveButton("确定") { _, _ ->
    val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
    startActivity(intent)
    }
    builder.show()
    }

    注意:就算获得了管理SD卡的权限,对于Android这个目录下的很多资源仍然是访问受限的。比如Android/data这个目录在Android 11是无法访问的,它放的是其他应用程序的数据信息。

Batch operations

Scoped Storage规定,每个应用程序都有权向MediaStore贡献数据,也可以权读取其他应用程序贡献的数据,但不能修改其他应用程序贡献的数据。但有些应用需要修改其他应用程序共享的数据(如美图秀秀、Photoshop等),针对这个问题,Android 10中提供了一种解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 作用:修改一张图片的灰度
*/
try {
contentResolver.openFileDescriptor(imageContentUri, "w")?.use {
Toast.makeText(this, "现在可以修改图片的灰度了", Toast.LENGTH_SHORT).show()
}
} catch (securityException: SecurityException) {//没有权限操作的情况会走底下的异常
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {//大于等于Android 10的异常类型是RecoverableSecurityException,是由于Scoped Storage限制导致操作没有权限的异常
val recoverableSecurityException = securityException as?
RecoverableSecurityException ?:
throw RuntimeException(securityException.message, securityException)

//获取一个intentSender对象,借助它进行页面跳转,引导用户手动授予我们修改这张图片的权限
val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
intentSender?.let {
startIntentSenderForResult(intentSender, IMAGE_REQUEST_CODE,
null, 0, 0, 0, null)
}
} else {
throw RuntimeException(securityException.message, securityException)
}
}

上述方法缺点是操作每张图片都要授权。Android 11中引入一个新功能Batch operations允许我们一次性对多个文件的操作权限进行申请。

关于Batch operations的用法也很好理解,Google一共提供了4种类型的权限申请API,如下所示:

  • createWriteRequest() 用于请求对多个文件的写入权限。
  • createFavoriteRequest() 用于请求将多个文件加入到Favorite(收藏)的权限。
  • createTrashRequest() 用于请求将多个文件移至回收站的权限。
  • createDeleteRequest() 用于请求将多个文件删除的权限。

createWriteRequest()举例:

1
2
3
4
5
6
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val urisToModify = listOf(uri1, uri2, uri3, uri4)
val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)//调用createWriteRequest()对多个文件集合,创建一个PendingIntent
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
null, 0, 0, 0)//调用startIntentSenderForResult进行权限申请
}

对于权限申请结果,在onActivityResult()中进行监听:

1
2
3
4
5
6
7
8
9
10
11
12
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EDIT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(this, "用户已授权", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "用户没有授权", Toast.LENGTH_SHORT).show()
}
}
}
}

上述的Android 10(依赖异常捕获机制)和Android 11(依赖Batch operations)的API用法不一样。如果不想在一个项目中写两套代码进行适配的话,那么做法是在代码中使用 Batch operations只针对 Android 11进行适配,在AndroidManifest.xml中添加requestLegacyExternalStorage标记让Android 10不需要适配。

示例代码

Android11开发手册