Android - 分区存储 MediaStore、SAF
一、概念
分区存储(Scoped Storage)的推出是针对 APP 访问外部存储的行为(乱建乱获取文件和文件夹)进行规范和限制,以减少混乱使得用户能更好的控制自己的文件。
公有目录被分为两大类:媒体文件(图片、音频、视频)的访问使用 MediaStore,其它文件通过系统的文件选择器访问 Storage Access Framework(简称SAF)。
二、MediaStore
跳转ContentProvider
| class MediaStore.Images | 所有图片内容的类。 |
| class MediaStore.Video | 所有视频内容的类。 |
| class MediaStore.Audio | 所有音频内容的类。 |
| class MediaStore.Files | 文件储存库中所有文件的索引,包括非媒体文件和媒体文件类。 |
| interface MediaStore.MediaColumns | 文件储存库中表的公共字段(文件的各种信息)。 |
2.1 获取 Uri
使用 Context 获取到 ContentResolver 对象,通过 Uri 即可获取各种媒体库的 ContentProvider,从而对媒体文件进行操作。
| 文件类型 | MediaStore 常量 | Uri 地址 |
| 图片 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI | content://media/external/images/media |
| 视频 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI | content://media/external/video/media |
| 音频 | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | content://media/external/audio/media |
| 非媒体文件 | MediaStore.Downloads.Media.EXTERNAL_CONTENT_URI | content://media/external/downloads |
val uri1 = Uri.parse("content://media/external/images/media")
val uri2 = MediaStore.Images.Media.getContentUri("external")
val uri3 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI //推荐
2.2 读取媒体文件
列名(文件信息)可以在 MediaStore.MediaColumns 取公共常量字段,也可以根据文件类型的不同在具体内部类中取值。
| 文件类型 | MediaStore 常量(常用列名) | 说明 |
| 图片 | MediaStore.Images.Media._ID | 磁盘上文件的路径 |
| MediaStore.Images.Media.DATA | 磁盘上文件的路径 | |
| MediaStore.Images.Media.DATE_ADDED | 文件添加到media provider的时间(单位秒) | |
| MediaStore.Images.Media.DATE_MODIFIED | 文件最后一次修改单元的时间 | |
| MediaStore.Images.Media.DISPLAY_NAME | 文件的显示名称 | |
| MediaStore.Images.Media.HEIGHT | 图像/视频的高度,以像素为单位 | |
| MediaStore.Images.Media.MIME_TYPE | 文件的 MIME 类型 | |
| MediaStore.Images.Media.SIZE | 文件的字节大小 | |
| MediaStore.Images.Media.TITLE | 标题 | |
| MediaStore.Images.Media.WIDTH | 图像/视频的宽度,以像素为单位 | |
| 视频 | MediaStore.Video.Media.TITLE | 名称 |
| MediaStore.Video.Media.DURATION | 总时长 | |
| MediaStore.Video.Media.DATA | 地址 | |
| MediaStore.Video.Media.SIZE | 大小 | |
| MediaStore.Video.Media.WIDTH | 视频的宽度,以像素为单位 | |
| MediaStore.Video.Media.HEIGHT | 视频的高度,以像素为单 | |
| 音频 | MediaStore.Audio.Media.TITLE | 歌名 |
| MediaStore.Audio.Media.ARTIST | 歌手 | |
| MediaStore.Audio.Media.DURATION | 总时长 | |
| MediaStore.Audio.Media.DATA | 地址 | |
| MediaStore.Audio.Media.SIZ | 大小 |
| public final Cursor query ( Uri uri, //要查询的 ContentProvider 的 Uri String[] projection, //要查询的字段(列Column),用 null 表示返回所有字段内容。 String selection, //查询条件,相当于SQL语句中的where,用 null 表示不进行筛选。 String[] selectionArgs, //如果 selection 里有?符号这里可以以实际值代替。没有的话可以为null。 String sortOrder //对结果进行排序,相当于SQL语句中的Order by,升序 asc /降序 desc,null为默认排序。 ) 返回的是一个封装了结果集的游标对象 Cursor ,资源用完需要调用 close() 关闭。 |
//获取图片类型的Uri
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
//要获取的信息(列名)
val projection = arrayOf(MediaStore.Images.Media._ID, //获取IDMediaStore.Images.Media.MIME_TYPE, //获取MIME_TYPEMediaStore.Images.Media.DISPLAY_NAME //获取DISPLAY_NAME
)
//筛选条件(png格式的图片)
val selection = "${MediaStore.Images.Media.DISPLAY_NAME}='.png'" // ='xx.png' 改成 =?
//筛选条件的参数
val selectionArgs = arrayOf(".png") //替换筛选条件语句中?部分
//对结果的排序方式
val sortOrder = "${ContactsContract.Contacts._ID} DESC" //注意:desc前有空格
//开始查询(返回的是一个封装了结果集的游标对象,资源用完需要关闭使用use函数)
contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->//表都是通过行和列定位到具体的位置然后数据将其取出cursor.run {//获取字段在第几列(查询什么才能取出什么,否则空指针异常)val idIndex = getColumnIndexOrThrow(MediaStore.Images.Media._ID)val mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)val displayNameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)//循环取出每一行对应字段的数据while (moveToNext()) {val id = getLong(idIndex)val mineType = getString(mimeTypeIndex)val displayName = getString(displayNameIndex)//合成图片的UriContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)//TODO...}}
}
//获取到的 Uri 可以通过 Glide 显示
Glide.with(context).load(uri).into(imageView)
//手动解析成图片的话
contentResolver.openFileDescriptor(uri, "")?.use {val bitmap = BitmapFactory.decodeFileDescriptor(it.fileDescriptor)imageView.setImageBitmap(bitmap)
}
2.3 写入媒体文件
通过 MediaStore 创建文件会保存到对应类型的默认目录中,也可以指定存放到其它同类型的公有目录或子文件夹中。如果存放到不同类型的公有目录中会报错 IllegalArgumentException(但是三种都可以存到Download中)。
| 文件类型 | mimeType 文件类型 | 默认存储目录(其它允许存储目录) |
| 图片 | image/* | Pictures(DICM) |
| 视频 | video/* | Movies(DICM) |
| 音频 | audio/* | Music(Alarms、Notifications、Podcasts、Ringtones) |
| 文件 | file/* | Download |
| public final Uri insert( Uri url, ContentValues values) 构造一个 ContentValues 对象通过 ContentResolver.insert 插入到对应的目录中,对返回的 Uri 对象进行文件流写入即可。 |
val values = ContentValues().apply {//指定 MimeTypeput(MediaStore.Images.Media.MIME_TYPE,"image/png")//指定文件名put(MediaStore.Images.Media.DISPLAY_NAME,"${System.currentTimeMillis()}.png")//指定保存的文件目录(如果不设置这个值,则会被默认保存到对应的媒体类型的文件夹下)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {//Android 10中新增了一个RELATIVE_PATH常量,表示文件存储的相对路径,可选值有DIRECTORY_DCIM、DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_MUSICput(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/DemoPicture")} else {//之前的系统版本中并没有RELATIVE_PATH,所以要使用 DATA 并拼装出一个文件存储的绝对路径才行put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}${File.separator}${Environment.DIRECTORY_DCIM}${File.separator}${System.currentTimeMillis()}.png")}
}
//插入文件数据库并获取到文件的Uri
val uri= contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
//对Uri进行文件流写入
uri?.let {//通过outputStream将本地图片bitmap或网络图片输入流写入UrlcontentResolver.openOutputStream(it)?.use { outputStream ->//TODO...//bitmap.compress(Bitmap.CompressFormat.PNG,100, outputStream)}
}
2.4 下载文件到Download目录
方式和上面的写入一样,将网络获取的输入流写入。
- 注意:MediaStore.Downloads是Android 10中新增的API,Android 9及以下的系统版本仍然使用之前的代码来进行文件下载。
val inputStream = XXX.inputStream
val bis = BufferedInputStream(inputStream)
val buffer = ByteArray(1024)//对Uri进行文件流写入
insertUri?.let {//通过outputStream将本地bitmap或网络输入流写入UrlcontentResolver.openOutputStream(it)?.use { outputStream ->BufferedOutputStream(outputStream).use { bos ->var bytes = bis.read(buffer)while (bytes >= 0) {bos.write(buffer, 0, bytes)bos.flush()bytes = bis.read(buffer)}}}
}
三、使用文件选择器 SAF
对于非媒体文件,无法像之前那样手写一个文件浏览器,而是必须使用系统提供的内置文件选择器。通过 Intent 启动系统的文件选择器,然后在 onActivityResult() 中获取到用户选中文件的 Uri 通过ContentResolver打开文件输入流来进行读取就可以了。
const val PICK_FILE = 1private fun pickFile() {val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)intent.addCategory(Intent.CATEGORY_OPENABLE)intent.type = "*/*"startActivityForResult(intent, PICK_FILE)
}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {PICK_FILE -> {if (resultCode == Activity.RESULT_OK && data != null) {val uri = data.dataif (uri != null) {val inputStream = contentResolver.openInputStream(uri)// 执行文件读取操作}}}}
}
四、第三方库不支持的解决办法
编写一个文件复制功能,将Uri对象所对应的文件复制到应用程序的关联目录下,然后再将关联目录下这个文件的绝对路径传递给第三方SDK,这样就可以完美进行适配了。
fun copyUriToExternalFilesDir(uri: Uri, fileName: String) {val inputStream = contentResolver.openInputStream(uri)val tempDir = getExternalFilesDir("temp")if (inputStream != null && tempDir != null) {val file = File("$tempDir/$fileName")val fos = FileOutputStream(file)val bis = BufferedInputStream(inputStream)val bos = BufferedOutputStream(fos)val byteArray = ByteArray(1024)var bytes = bis.read(byteArray)while (bytes > 0) {bos.write(byteArray, 0, bytes)bos.flush()bytes = bis.read(byteArray)}bos.close()fos.close()}
}
五、管理设备上所有的文件(公有目录 + 自定义目录)
绝大部分的应用程序都不应该申请这个权限,仅适用于文件浏览器、病毒查杀类APP,需要跳转到系统页面让用户手动授权,Play商店上架也会更严格。即便得到授权也只能访问 公有目录 + 自定义目录,依然无法访问私有目录。
5.1 权限声明
//不加 ignore 属性 AndroidStudio 会用警告提醒。
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" />
5.2 跳转系统页面授权
//系统低于11或者方法返回true说明已经拥有整个SD卡管理权限
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()
}
六、修改其它APP贡献的文件
修改其它APP贡献的文件是不安全的行为,默认情况下会抛异常,需要跳转到系统页面让用户手动授权,仅适用于美图秀秀类APP。在 Android 10 中每次跳转授权只能操作一张图片,如果一个程序需要修改很多张图片会很麻烦,在 Android 11 中提供了 Batch Operations 从而一次性对多个文件的操作权限进行申请。
- 由于10 之前没有分区存储,10 和 11以后是两套处理方案,专门针对 10 一个版本去写处理方案会很麻烦,由于 10 不是强制启用分区存储,可以在 AndroidManifest 中配置 requestLegacyExternalStorage 来禁用。
| createWriteRequest() 请求对多个文件的写入权限。 |
| createFavoriteRequest() 请求将多个文件加入到Favorite(收藏)的权限。 |
| createTrashRequest() 请求将多个文件移至回收站的权限。 |
| createDeleteRequest() 请求将多个文件删除的权限。 |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {/创建了一个集合用于存放所有要批量申请权限的文件Urival urisToModify = listOf(uri1, uri2, uri3, uri4)//创建一个PendingIntentval editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)//进行权限申请startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE, null, 0, 0, 0)
}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 - 分区存储 MediaStore、SAF
官方页面 参考文章 一、概念 分区存储(Scoped Storage)的推出是针对 APP 访问外部存储的行为(乱建乱获取文件和文件夹)进行规范和限制,以减少混乱使得用户能更好的控制自己的文件。 公有目录被分为两大类:…...
Shiro框架权限控制
首先去通过配置类的用户认证,在用户认证完成后,进行用户授权,用户通过授权之后再跳转其他的界面时,会进行一个验证,当前账号是否有权限。 前端权限控制显示的原理 在前端中,通常使用用户的角色或权限信息来…...
centOS7 安装tailscale并启用子网路由
1、在centOS7上安装Tailscale客户端 #安装命令所在官网位置:https://tailscale.com/download/linux #具体命令为: curl -fsSL https://tailscale.com/install.sh | sh #命令执行后如下图所示2、设置允许IP转发和IP伪装。 安装后,您可以启动…...
spring 项目中如何处理跨越cors问题
1.使用 CrossOrigin 注解 作用于controller 方法上 示例如下 RestController RequestMapping("/account") public class AccountController {CrossOriginGetMapping("/{id}")public Account retrieve(PathVariable Long id) {// ...}DeleteMapping(&quo…...
importlib --- import 的实现
3.1 新版功能. 源代码 Lib/importlib/__init__.py 概述 importlib 包具有三重目标。 一是在 Python 源代码中提供 import 语句的实现(并且因此而扩展 __import__() 函数)。 这提供了一个可移植到任何 Python 解释器的 import 实现。 与使用 Python 以…...
【PyTorch】现代卷积神经网络
文章目录 1. 理论介绍1.1. 深度卷积神经网络(AlexNet)1.1.1. 概述1.1.2. 模型设计 1.2. 使用块的网络(VGG)1.3. 网络中的网络(NiN)1.4. 含并行连结的网络(GoogLeNet)1.5. 批量规范化…...
用python编写九九乘法表
1 问题 我们在学习一门语言的过程中,都会练习到编写九九乘法表这个代码,下面介绍如何编写九九乘法表的流程。 2 方法 (1)打开pycharm集成开发环境,创建一个python文件,并编写第一行代码,主要构建…...
Google Gemini 模型本地可视化
Google近期发布了Gemini模型,而且开放了Gemini Pro API,Gemini Pro 可免费使用! Gemini Pro支持全球180个国家的38种语言,目前接受文本、图片作为输入并生成文本作为输出。 Gemini Pro的表现超越了其他同类模型,当前版…...
数据修复:.BlackBit勒索病毒来袭,安全应对方法解析
导言: 黑色数字罪犯的新玩具——.BlackBit勒索病毒,近来成为网络安全领域的头号威胁。这种恶意软件以其高度隐秘性和毁灭性而引起广泛关注。下面是关于.BlackBit勒索病毒的详细介绍,如不幸感染这个勒索病毒,您可添加我们的技术服…...
拓扑排序实现循环依赖判断 | 京东云技术团队
本文记录如何通过拓扑排序,实现循环依赖判断 前言 一般提到循环依赖,首先想到的就是Spring框架提供的Bean的循环依赖检测,相关文档可参考: https://blog.csdn.net/cristianoxm/article/details/113246104 本文方案脱离Spring Be…...
Java的NIO工作机制
文章目录 1. 问题引入2. NIO的工作方式3. Buffer的工作方式4. NIO数据访问方式 1. 问题引入 在网络通信中,当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStream和OutputStream,并通过这…...
一个简单的光线追踪渲染器
前言 本文参照自raytracing in one weekend教程,地址为:https://raytracing.github.io/books/RayTracingInOneWeekend.html 什么是光线追踪? 光线追踪模拟现实中的成像原理,通过模拟一条条直线在场景内反射折射,最终…...
C++学习笔记(十二)------is_a关系(继承关系)
你好,这里是争做图书馆扫地僧的小白。 个人主页:争做图书馆扫地僧的小白_-CSDN博客 目标:希望通过学习技术,期待着改变世界。 提示:以下是本篇文章正文内容,下面案例可供参考 文章目录 前言 一、继承关系…...
DC电源模块的设计与制造技术创新
BOSHIDA DC电源模块的设计与制造技术创新 DC电源模块的设计与制造技术创新主要涉及以下几个方面: 1. 高效率设计:传统的DC电源模块存在能量转换损耗较大的问题,技术创新可通过采用高效率的电路拓扑结构、使用高性能的功率开关器件和优化控制…...
Sketch for Mac:实现你的创意绘图梦想的矢量绘图软件
随着数字时代的到来,矢量绘图软件成为了广告设计、插画创作和UI设计等领域中必不可少的工具。在众多矢量绘图软件中,Sketch for Mac(矢量绘图软件)以其强大的功能和简洁的界面脱颖而出,成为了众多设计师的首选。 Sket…...
ReactNative0.73发布,架构升级与更好的调试体验
这次更新包含了多种提升开发体验的改进,包括: 更流畅的调试体验: 通过 Hermes 引擎调试支持、控制台日志历史记录和实验性调试器,让调试过程更加高效顺畅。稳定的符号链接支持: 简化您的开发工作流程,轻松将文件或目录链接到其他…...
SVN忽略文件的两种方式
当使用版本管理工具时,提交到代码库的文档我们不希望存在把一些临时文件也推送到仓库中,这样就需要用到忽略文件。SVN的忽略相比于GIT稍显麻烦,GIT只需要在.gitignore添加忽略规则即可。而SVN有两种忽略方式,一个是全局设置&#…...
手写VUE后台管理系统10 - 封装Axios实现异常统一处理
目录 前后端交互约定安装创建Axios实例拦截器封装请求方法业务异常处理 axios 是一个易用、简洁且高效的http库 axios 中文文档:http://www.axios-js.com/zh-cn/docs/ 前后端交互约定 在本项目中,前后端交互统一使用 application/json;charsetUTF-8 的请…...
JavaScript装饰者模式
JavaScript装饰者模式 1 什么是装饰者模式2 模拟装饰者模式3 JavaScript的装饰者4 装饰函数5 AOP装饰函数6 示例:数据统计上报 1 什么是装饰者模式 在程序开发中,许多时候都我们并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就…...
C++学习笔记01
01.C概述(了解) c语言在c语言的基础上添加了面向对象编程和泛型编程的支持。 02.第一个程序helloworld(掌握) #define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std;//标准命名空间int main() {//co…...
超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
HTML 列表、表格、表单
1 列表标签 作用:布局内容排列整齐的区域 列表分类:无序列表、有序列表、定义列表。 例如: 1.1 无序列表 标签:ul 嵌套 li,ul是无序列表,li是列表条目。 注意事项: ul 标签里面只能包裹 li…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
如何为服务器生成TLS证书
TLS(Transport Layer Security)证书是确保网络通信安全的重要手段,它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书,可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
Caliper 配置文件解析:config.yaml
Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
