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

距离 Android 11 正式发布已经半年有余,也该是时候写写 Android 11 新特性这方面的文章了。android

当初我有大概了解过一些 Android 11 上的行为变动,整体变化虽然很多,可是要求咱们必须去适配的地方并不算多。其中一个可能须要适配的地方是 Android 11 的权限变动,关于这部份内容我在 PermissionX 如今支持 Java 了!还有 Android 11 权限变动讲解 这篇文章中已经作了比较详细的讲解。git

除此以外,在 Scoped Storage 这块,Android 11 上又有了一些新的变化,本篇文章咱们就重点来讨论一下这部份内容。github

Scoped Storage

事实上,Scoped Storage 并非 Android 11 上推出的新功能,而是在 Android 10 中就已经有了,而且我当时还专门写了一篇文章讲解此功能,能够参考 Android 10 适配要点,做用域存储浏览器

不用担忧,以前这篇文章中介绍的内容并无过期。当时在 Android 10 上可使用的功能,如今在 Android 11 上依然可使用,只不过 Android 11 对于 Scoped Storage 又作了一些丰富与扩展。那么毫无疑问,这就是咱们本篇文章的重点。安全

强制启用 Scoped Storage

首先,在 Android 11 中,Scoped Storage 被强制启用了。微信

那么强制启用是什么意思呢?markdown

在 Android 10 中虽然也有 Scoped Storage 功能,可是 Google 考虑到广大应用程序适配也是须要时间的,所以并无强制启用这个功能。app

只要应用程序指定的 targetSdkVersion 低于 29,或 targetSdkVersion 等于 29,但在 AndroidManifest.xml 中加入了以下配置:ide

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

复制代码

那么 Scoped Storage 功能就不会被启用。函数

在 Android 11 中以上配置依然有效,但仅限于 targetSdkVersion 小于或等于 29 的状况。若是你的 targetSdkVersion 等于 30,Scoped Storage 就会被强制启用,requestLegacyExternalStorage 标记将会被忽略。

那么强制启用了 Scoped Storage 以后对开发者而言有什么影响吗?

其实若是你的应用程序已经按照 Android 10 适配要点,做用域存储 这篇文章中讲解的方式对 Scoped Storage 进行了适配,那么恭喜你,如今你什么都不须要作,就已经可以适配 Android 11 系统了。

也就是说,对于绝大部分开发者而言,强制启用 Scoped Storage 其实并无什么影响,只要你的应用程序在以前已经适配了 Android 10 的 Scoped Storage。

可是有一类应用程序很是特殊,就是文件浏览器,如 Root Explorer、ES Explorer 等。这类程序自己提供的功能就是对 SD 上的文件进行浏览与管理,而强制启用了 Scoped Storage 以后,本质上就没有文件浏览的概念了,咱们也没法以文件的真实路径来对文件进行管理。

从这个角度上看,Scoped Storage 对于文件浏览器类的程序形成了毁灭性的打击。不过不用担忧,Google 仍然仍是给这类程序提供了另一种解决方案,下面咱们就来学习一下。

管理设备上全部的文件

首先明确一点,Android 11 中强制启用 Scoped Storage 是为了更好地保护用户的隐私,以及提供更加安全的数据保护。对于绝大部分应用程序来讲,使用 MediaStore 提供的 API 就已经能够知足你们的开发需求了。若是你没有相似于开发文件浏览器这种需求,请尽量不要使用接下来即将介绍的技术。

拥有对整个 SD 卡的读写权限,在 Android 11 上被认为是一种很是危险的权限,同时也可能会对用户的数据安全形成比较大的影响。

但文件浏览器就是要对设备的整个 SD 卡进行管理的,这怎么办呢?对于这类危险程度比较高的权限,Google 一般采用的作法是,使用 Intent 跳转到一个专门的受权页面,引导用户手动受权,好比悬浮窗,无障碍服务等。

没错,在 Android 11 中,若是你想要管理整个设备上的文件,也须要使用相似的技术。

首先,你必须在 AndroidManifest.xml 中声明 MANAGE_EXTERNAL_STORAGE 权限,以下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.scopedstoragedemo">

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

</manifest>
复制代码

注意相比于传统声明一个权限,这里增长了 tools:ignore="ScopedStorage" 这样一个属性。由于若是不加上这个属性,Android Studio 会用一个警告提醒咱们,绝大部分的应用程序都不该该申请这个权限,正如我前面介绍的同样。

接下来的工做也至关简单,咱们可使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 这个 action 来跳转到指定的受权页面,能够经过 Environment.isExternalStorageManager() 这个函数来判断用户是否已受权,下面我写了一段比较简单的代码来演示这个功能:

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()
}
复制代码

能够看到,这里首先判断若是系统版本低于 Android 11,或者 Environment.isExternalStorageManager() 返回 true,那么就说明咱们已经拥有管理整个 SD 卡的权限了。如今你能够直接使用传统的写法,以文件真实路径的形式对文件进行操做。

而若是尚未管理 SD 卡的权限,则会弹出一个对话框,告知用户申请权限的缘由,而后使用 Intent 跳转到指定的受权页面,让用户手动进行受权。

程序的运行效果以下图所示:

有了这个权限以后,你就能够用过去熟知的方式去开发文件浏览器了。

不过还有一点须要注意,即便咱们得到了管理 SD 卡的权限,对于 Android 这个目录下的不少资源仍然是访问受限的,好比说 Android/data 这个目录在 Android 11 中使用任何手段都没法访问。由于不少应用程序的数据信息都会存放在这个目录下,作这个限制的目的主要仍是考虑到用户的数据安全吧。否则的话,容许微信去读取淘宝中的数据,怎么想好像都是不合适的。

Batch operations

下面咱们再来看 Android 11 中关于 Scoped Storage 的另一个新特性。

Scoped Storage 规定,每一个应用程序都有权限向 MediaStore 贡献数据,好比说插入一张图片到手机相册当中。也有权限读取其余应用程序所贡献的数据,好比说获取手机相册中的全部图片。这些功能我在 Android 10 适配要点,做用域存储 这篇文章中都进行了演示。

可是,假如你要修改其余应用程序所贡献的数据,那很差意思,Scoped Storage 是不容许你这样作的。

缘由也很简单,若是一张图片是你插入到手机相册的,你固然有权限对它进行任意修改。可是若是这张图片是其余应用程序插入到手机相册的,你还能对它进行任意修改,这在 Google 看来就又是一个安全隐患,因此 Scoped Storage 限制了这个功能。

不过,若是有些应用程序就是须要修改别的应用所贡献的数据呢?这种例子也不难找,好比 Photoshop、美图秀秀等,它们的目的就是为了修改手机相册中的图片,无论这个图片是否是它们本身所建立的。

针对这个问题,Android 10 中提供了一种解决方案:

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) {
        val recoverableSecurityException = securityException as?
            RecoverableSecurityException ?:
            throw RuntimeException(securityException.message, securityException)

        val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
        intentSender?.let {
            startIntentSenderForResult(intentSender, IMAGE_REQUEST_CODE,
                    null, 0, 0, 0, null)
        }
    } else {
        throw RuntimeException(securityException.message, securityException)
    }
}
复制代码

下面我来简单解释一下这段代码。

首先这段代码的目的是为了修改一张图片的灰度,但因为这张图片并非由当前应用程序所贡献的,因此理论上当前应用程序并无权限去修改这张图片的灰度。

那么明明没有权限去修改,可是咱们仍是执意去修改会发生什么状况呢?这个很好理解,固然是抛异常了。因而这里用 try catch 的方式包裹了修改图片灰度的操做,而后在 catch 的代码块中判断,若是当前系统版本大于等于 Android 10,而且异常的类型是 RecoverableSecurityException,那么就说明这是一个因为 Scoped Storage 限制致使操做没有权限的异常。

接下来会从 RecoverableSecurityException 对象中获取一个 intentSender,再借助这个 intentSender 进行页面跳转,引导用户手动授予咱们修改这张图片的权限。运行效果以下:

这种方式虽然可行,但却有一个很是明显的缺点:每次咱们只能操做一张图片。若是一个程序须要修改不少张图片,没有什么好办法,只能每张图片都用上述方式去申请权限。

相信 Google 也是意识到了这个问题,因而在 Android 11 中引入了一个新的功能,叫做 Batch operations,从而容许咱们能够一次性对多个文件的操做权限进行申请。

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

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

其中最经常使用的主要是 createWriteRequest() 和 createDeleteRequest() 这两个接口,这里咱们以 createWriteRequest() 举例。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    val urisToModify = listOf(uri1, uri2, uri3, uri4)
    val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)
    startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
            null, 0, 0, 0)
}
复制代码

代码很是简单,首先咱们建立了一个集合,用于存放全部要批量申请权限的文件 Uri,而后调用 createWriteRequest() 函数去建立一个 PendingIntent,接下来再调用 startIntentSenderForResult 进行权限申请便可。

关于权限申请的结果,咱们能够在 onActivityResult() 中进行监听:

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()
            }
        }
    }
}
复制代码

程序的运行结果以下图所示:

其它几个 API 的用法都是彻底相同的,这里就再也不重复举例了。

看到这里,有的朋友可能会说,Android 10 和 Android 11 提供的 API 彻底不一样,Android 10 是要依赖于异常捕获机制,从 RecoverableSecurityException 中解析出 intentSender,而 Android 11 能够借助 Batch operations 提供的 API 直接建立 intentSender。我该不会须要在一个项目中针对 Android 10 和 Android 11 分别写两套代码去进行适配吧?

这确实是个头疼的问题,并且我以为主要是因为 Google 一开始在 Android 10 中 API 设计不合理所致使的。依赖于异常捕获机制的方案,不管如何都不能说是一种出色的 API 设计。

不过随着后来更多的思考,我发现这并非一个没法解决的问题,而且解决方案还很是简单。

为何呢?别忘了,Android 10 中的 Scoped Storage 并非强制启用的,咱们能够在 AndroidManifest.xml 中配置 requestLegacyExternalStorage 标记来禁用 Scoped Storage。这样的话,Android 10 就是不须要适配的,咱们只须要在 Android 11 中使用更加科学规范的 API 来进行 Scoped Storage 适配就能够了。

好了,本篇文章就到这里,文中全部的代码示例我都写成了一个 Demo,放到了 GitHub 上,有须要的朋友能够到如下网址查看:

github.com/guolindev/S…

另外,若是想要学习 Kotlin 和最新的 Android 知识,能够参考个人新书 《第一行代码 第 3 版》

关注个人技术公众号“郭霖”,每一个工做日都有优质技术文章推送。

相关文章
相关标签/搜索