共享存储中的新功能

什么是共享存储

也就是外部存储,是一片应用得到了读写权限之后可以写入可被其他应用看到的文件的区域。下面是一些可以得到外部存储位置的方法:

  • Context.getExternalFilesDirs()

    比如,/sdcard/Android/data/{packagename}/files

  • Context.getExternalCacheDirs()

    比如,/sdcard/Android/data/{packagename}/cache

  • Context.getExternalMediaDirs()

    比如,/sdcard/Android/media/{packagename}

  • Context.getObbDirs()

    比如,/sdcard/Android/obb/{packagename}

Android 9 上存储是怎么工作的

外部存储是一个供用户和应用查看和修改的全局可读的空间,这个空间也就是你把手机通过数据线连接到电脑上并选择文件传输模式时看到的那块硬盘。当应用把文件写入外部存储时,文件并不单独处属于该应用。但是有一个例外,就是外部存储上应用的归属目录,就是我们上面提到的几个方法返回的位置,应用在访问自己的这些目录时是不需要申请权限的,应用的归属目录在应用被卸载会被清空。

当获得 READ/WRITE_EXTERNAL_STORAGE 权限后,才可以访问共享存储,包括 SD 卡下的所有位置,同时能够查询 MediaStore。

改善外部存储

提出了三个原则来改善外部存储,以此来提升用户体验。

  • 更好的归属

    在之前,应用可能在 SD 卡上生成​乱糟糟的文件,即使应用卸载后这些文件依然保留在 SD 卡上,而实际上这些文件只是和应用相关的,用户不需要保留。在 Android Q 中,系统知道什么文件是属于什么应用的,在应用的卸载的时候就会把这些文件清除。当然还有些文件是在应用卸载后用户想保留下来的,我们稍后会讲到。

  • 保护应用数据

    当应用往外部存储写入文件的时候,不应该使其这些文件让其他应用随意访问。在 Q 上,应用特有文件是不允许其他应用查看的。

  • 保护用户数据

    当你下载了一些文件之后,比如和朋友聊天当中的一些比较私密的图片或你的 pdf 格式的信用卡账单,你应该不会愿意所有应用都能立刻看到这些文件并以此来搜集你的行为。在 Q 上,可以确保用户知道什么时候什么文件被访问了。

Android Q 上存储是怎么工作的

Android Q 重点加强了 MediaStore。应用无需任何权限即可向 MediaStore 中写入文件,并且可以无需权限即可查看自己写入的文件,但是,如果要查看其他应用写入的文件,必须获取存储权限。应用仍然可以读写它们的专有目录,但是,其他应用即使获取了存储权限也不能查看。应用仍然可以通过系统选择器选择任意的文件,这会使访问更安全,因为它使用户更明确的知道什么文件或哪些文件正在被访问。

MediaStore

MediaStore 对 Android 来说并不陌生,事实上,它从第一天开始就存在了,它是属于用户的通用媒体集合,在 Android Q 上重点加强并改善了它。

什么属于 MediaStore?

MediaStore 旨在保存属于用户的内容,并且这些内容是用户希望在其他应用中能看到的。所以建议只在 MediaStore 中保存用户主动想保存在其中的东西,比如通过相机拍的照片。在 MediaStore 中有三种主要的媒体类型:MediaStore.Audio,MediaStore.Video 和 MediaStore.Images。插入到 MediaStore 中的内容在应用卸载后不会被删除,但是,一旦应用卸载了,它之前插入到 MediaStore 的内容则被认为不在属于该应用,如果后面重新安装了该应用,如果它要查看它之前插入的内容的话,必须从用户那里获得存储权限授权。

什么不属于 MediaStore?

更多和用户的期望有关,但有一些情况很明显,应用特有的一些文件,比如一个音乐应用中的专辑封面,通常是不适合放到 MediaStore 中的,因为它们不是用户想要在其他应用中看到的,或者在应用卸载后想要保留下来的文件。在某些情况下,你可能不知道文件应该放到哪里,比如在一个聊天 app 中,两个人可能互相发了大量的图片,这些图片如果都放到 MediaStore 中,就会使 MediaStore 变得一团糟,在这种情况下,建议不要立即把这些文件都放到 MediaStore 中,直到用户采取了明确的行动,比如点击保存按钮,再把文件放到 MediaStore 中。

MediaStore.Downloads

Android Q 中引入了一种新的 MediaStore 类型,downloads,它的工作方式和之前提到的三种类型有点不一样,用于存放不适合放到其他类型集合中的文件,比如 PDF 文件或其他任意类型的文件。对于其他其他类型的文件,如果你想显式地把它放到 downloads 集合中的话也是可以的,在这种情况下,它们会被同时放入对应类型的集合中。比如图片,如果你从浏览器下载了一张图片,它是会被放到 downloads 集合中的,但同时也会出现在 images 集合中。

在讲前面的几种集合类型的时候说到了,应用可以查看它们自己创建的文件而无需存储权限,downloads 集合也是一样的。但是不一样的是,如果要查看其他应用贡献的文件,前面几种类型只要获取存储权限就可以了,但对 downloads 集合来说,有存储权限也访问不到其他应用放到 downloads 中的文件,必须使用系统选择器(比如通过 ACTION_OPEN_DOCUMENT 启动,也就是 SAF)。

另一个建议是,不要永远把文件放到 downloads 集合中,尤其是用户想要控制文件的存储位置的时候,比如保存一个文档,这种情况建议使用 ACTION_CREATE_DOCUMENT,它可以让用户选择任意的本地或云存储提供程序的位置。

实例

代码地址:https://github.com/niniliwei/QStorageDemo

写入文件

上面说到了,在 Android Q 中你可以向 MediaStore 中写入文件而无需申请存储权限,你现在可以插入、修改或删除 MediaStore 中任何属于你自己的文件。只有当你需要访问其他应用创建的文件时,才需要在运行时申请存储权限。当你向 MediaStore 中写入文件之后,这些文件在应用的整个生命周期中都属于你。如果用户卸载了你的应用,你之前保存的数据依然会被保留。下面是一个示例。

// 在用户的集合中创建一个新的图片
val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "IMG1024.JPG")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    put(MediaStore.Images.Media.IS_PENDING, 1)
}

val resolver = context.getContentResolver()
val collection = MediaStore.Images.Media
        .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item = resolver.insert(collection, values)

// 向我们的图片中写入数据
resolver.openFileDescriptor(item, "w", null).use { pfd ->
    // ...
}

// 现在数据写入完毕,可以让其他应用查看了
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(item, values, null, null)

其中 IS_PENDING 列是 Q 新增的列,将它的值设置为 1 后,文件暂时对其他应用不可见,将它的值设置为 0 才可以被其他应用看到。

指定目录和设备

系统会根据文件类型来组织新创建的文件,比如,image/* 类型的文件默认会被放到 Pictures 目录中。

你可以通过设置 MediaColumns.RELATIVE_PATH 来指定文件放在那里。你也可以通过修改 RELATIVE_PATH 或 DISPLAY_NAME 调用 ContentResolver.update() 方法来移动文件位置。

从 Q 开始,通过指定设备名,可以在附加存储设备上创建文件了。MediaStore.VOLUME_EXTERNAL 是主存储设备,其他存储设备名称可以通过 MediaStore.getExternalVolumeNames() 来获取。

通过在任意的 MediaStore.*.getContentUri() 方法传入具体的设备名,就可以在指定的存储设备上操作文件了。

// 在指定存储设备的指定目录中插入一个音频文件
val values = ContentValues().apply {
    put(MediaStore.Audio.Media.RELATIVE_PATH,
         "Music/My Artist/My Albumn")
    put(MediaStore.Audio.Media.DISPLAY_NAME,
         "My Song.mp3")
}

val volumeNames = MediaStore.getExternalVolumeName(context)
// ...
val collection = MediaStore.Audio.Media.getContentUri(volumeName)
val item = resolver.insert(collection, values)

注意,要插入的文件类型必须和 MediaStore 的类型一致,否则系统会发生异常,比如如果你想把一个 mp3 文件插入 images 中就不行,这一切就是希望所有的文件能待在它应该待的地方。

检索文件

你可以通过 ContentResolver.query() 方法来检索文件,并通过 ContentResolver.openFileDescriptor() 方法来读写文件。原来的的 DATA 列已经被废弃了,不要在使用了。

Android Q 之前:

/storage/emulated/0/DCIM/Camera/IMG_20190531_163446.jpg

Android Q:

/mnt/content/media/external/images/media/1075

待定(pending)条目默认被隐藏,但可以通过 MediaStore.setIncludePending() 方法使他们显示出来。

不包含(默认)待定条目的集合 uri:content://media/external/images/media

包含待定条目的集合 uri:content://media/external/images/media?includePending=1

其他应用的处于 pending 状态的文件是无法打开的,直到被发布。

// 查找所有视频文件(包括已发布的和处于 pending 状态的)
val collection = MediaStore.Video.Media.getContentUri(volumeName)
val collectionWithPending = MediaStore.setIncludePending(collection)
resolver.qury(collectionWithPending, null, null, null).use { c ->
    // ...
}

// 打开一个媒体项目
resolver.openFileDescriptor(item, mode).use { pfd ->
    // ...
}

// 加载指定媒体项目的缩略图
val thumb = resolver.loadThumbnail(item, Size(640, 480), null)

编辑

我们之前说过了,你对自己贡献的文件拥有完全的访问权限,不管是读、写还是删除,但是,对于属于其他应用的文件,你只有读取权限,如果你想修改其他应用的文件,你需要得到用户的同意,任何 ContentResolver 和修改文件相关的操作,不管是 update()delete() 还是 openFileDescriptor() 的写入模式,内部都会检查你是否有那个文件的相应权限,如果你没有权限,会抛出 RecoverableSecurityException,这是一个新的异常类型,这是在告诉你你没有操作权限,需要你告诉用户,让用户将权限授予给你。

try {
    resolver.delete(item, null, null)
} catch (e: RecoverableSecurityException) {
    AlertDialog.Builder(requireActivity())
        .setMessage(e.userMessage)
        .setPositiveButton(e.userAction.title) { dialog, which ->
            try {
                e.userAction.actionIntent.send()
            } catch (ignored: PendingIntent.CanceledException) {
            }
        }
        .setNegativeButton(android.R.string.cancel, null)
        .show()
}

元数据

Android Q 会保护媒体文件中的位置元数据。如果你需要访问位置数据,需要吧 ACESS_MEDIA_LOCATION 添加的你的应用中,这是 Q 新加的权限,它不是一个运行时权限,只要声明就可以了。

MediaStore 中的 LATITUDE 和 LONGITUDE 列现在已经废弃了,使用 ExifInterface 替代。

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

val latLong = resolver.openInputStream(item).use { stream ->
    ExifInterface(stream).run {
        latLong ?: doubleArrayOf(0.0, 0.0)
    }
}

Scoped Storage

上面提到了三项准则,为了达到这些目标,从 Android Q 开始限制对原始文件系统的直接访问,通过一项叫 scoped storage 的特性来实现。

在 Q 上,如果你的应用的目标版本是旧的 API 等级,也就 Q 以下,你不会看到任何变化。你仍然可以在得到外部存储设备的访问权限之后,进行原始文件系统级别的访问。

当你的应用以 Q 为目标版本时,它会被放进 “scoped” 存储模式,这意味着它不能通过原始文件系统再直接访问 SD 卡下的目录,如果尝试直接访问的话可能会导致 FileNotFoundException 。

你继续拥有应用特有目录的直接访问权限,如果要和应用特有目录之外的位置打交道(work with)的话,需要使用 MediaStore 或 SAF。

什么时候应该用哪个呢?MediaStore 适合强类型的内容,像我们说过的图片,音频和视频,但不适合其他内容类型,比如 Word 文档,Excel 表格或 PDF 文件,这些内容需要启动一个意图,比如 SAF,来让用户选择需要的内容。SAF 从 4.4 开始就有了,相较于实现自己的文件选择器需要用户学习怎么使用,使用 SAF 的话,在所有的应用中会有一个统一的体验。MediaStore 还有一个限制,只能和设备上的本地内容打交道,它不能访问设备之外的其他存储地址,而这正好是 SAF 的强大功能,它能让用户使用云存储提供程序。SAF 是一个开放的平台,从 4.4 开始,有一个 DocumentsProvider API,作为三方开发者,如果你想为用户提供一个存储内容的地方,你可以成为这些 DocumentsProvider 中的一个。

如果应用的目标版本为 Q,scoped storage 默认是启用的,如果你没有做好准备的话,可以选择暂时禁用。注意,在明年的 Android 版本中,不管你应用的目标版本是什么,scoped storage 模式都会启用,所以建议提前适配。

<!-- 目标版本为 Q 时暂时禁用 scoped storage 模式 -->
<uses-sdk android:targetSdkVersion="29" />
<application
   android:requestLegacyExternalStorage="true" />

// 检查你的应用运行在什么模式下
if (Environment.isExternalStorageLegacy()) {
    // ...
}

参考

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值