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_URIcontent://media/external/images/media
视频MediaStore.Video.Media.EXTERNAL_CONTENT_URIcontent://media/external/video/media
音频MediaStore.Audio.Media.EXTERNAL_CONTENT_URIcontent://media/external/audio/media
非媒体文件MediaStore.Downloads.Media.EXTERNAL_CONTENT_URIcontent://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()}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/215491.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

会员运营常用的ChatGPT通用提示词模板

会员体系&#xff1a;如何建立和完善会员体系&#xff1f; 会员等级&#xff1a;如何设定会员等级及权益&#xff1f; 会员留存&#xff1a;如何提高会员留存率&#xff1f; 会员活跃度&#xff1a;如何提高会员活跃度&#xff1f; 会员招募&#xff1a;如何招募新会员&…

ubuntu install sqlmap

refer: https://github.com/sqlmapproject/sqlmap 安装sqlmap&#xff0c;可以直接使用git 克隆整个sqlmap项目&#xff1a; git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev 2.然后进入sqlmap-dev&#xff0c;使用命令&#xff1a; python s…

静态代理IP搭建步骤,静态匿名在线代理IP如何使用?

静态代理搭建步骤 1. 确定需求 在搭建静态代理之前&#xff0c;需要明确自己的需求&#xff0c;包括代理服务器的位置、访问速度、匿名性、安全性等方面的要求。 2. 选择代理服务器提供商 可以选择自己购买服务器搭建代理&#xff0c;也可以选择使用云服务提供商的代理服务…

【Python百宝箱】探索强化学习算法的利器:航行在AI之海的罗盘指南

强化学习的工具宝盒&#xff1a;探索各色瑰宝&#xff0c;点亮智能之旅 前言 人工智能和强化学习正成为推动科技进步的重要力量。在这个领域中&#xff0c;使用适当的库和工具可以加速算法研发和应用部署的过程。本文将深入探索一系列具有代表性的强化学习库和工具&#xff0…

有趣的数学 用示例来阐述什么是初值问题二

一、示例 解决以下初值问题。 解决这个初始值问题的第一步是找到一个通用的解决方案。为此&#xff0c;我们找到微分方程两边的反导数。 即 我们能够对两边进行积分&#xff0c;因为y项是单独出现的。请注意&#xff0c;有两个积分常数&#xff1a;C1和C2。求解前面的方程y给出…

电工--半导体器件

目录 半导体的导电特性 PN结及其单向导电性 二极管 稳压二极管 双极型晶体管 半导体的导电特性 本征半导体&#xff1a;完全纯净的、晶格完整的半导体 载流子&#xff1a;自由电子和空穴 温度愈高&#xff0c;载流子数目愈多&#xff0c;导电性能就愈好 型半导体&…

28. Python Web 编程:Django 基础教程

目录 安装使用创建项目启动服务器创建数据库创建应用创建模型设计路由设计视图设计模版 安装使用 Django 项目主页&#xff1a;https://www.djangoproject.com 访问官网 https://www.djangoproject.com/download/ 或者 https://github.com/django/django Windows 按住winR 输…

docker build构建报错:shim error: docker-runc not installed on system

问题&#xff1a; docker构建镜像时报错&#xff1a;shim error: docker-runc not installed on system 解决&#xff1a; ln -s /usr/libexec/docker/docker-runc-current /usr/bin/docker-runc

MySQL数据库——锁-表级锁(表锁、元数据锁、意向锁)

目录 介绍 表锁 语法 特点 元数据锁 介绍 演示 意向锁 介绍 分类 演示 介绍 表级锁&#xff0c;每次操作锁住整张表。锁定粒度大&#xff0c;发生锁冲突的概率最高&#xff0c;并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。 对于表级锁&#xff0c;主要…

选择排序和堆排序

目录 前言 一.选择排序 1.思想 2.实现 3.特点 二.堆排序 1.思想 2.实现 3.特点 前言 排序算法是计算机科学中的基础工具之一&#xff0c;对于数据处理和算法设计有着深远的影响。了解不同排序算法的特性和适用场景&#xff0c;能够帮助程序员在特定情况下…

【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统(一)搭建项目

前言 最近两个月一直在忙公司的项目&#xff0c;上班时间经常高强度写代码&#xff0c;下班了只想躺着&#xff0c;没心思再学习、做自己的项目了。最近这几天轻松一点了&#xff0c;终于有时间 摸鱼了 做自己的事了&#xff0c;所以到现在我总算是搭起来一个比较完整的后台管…

nrfutil工具安装

准备工作&#xff0c;下载相关安装包 链接&#xff1a;https://pan.baidu.com/s/1LWxhibf8LiP_Cq3sw0kALQ 提取码&#xff1a;2dlc 解压后&#xff0c;分别安装以下安装包 在C盘下创建目录nordic_tools&#xff0c;并将nrfutil复制到刚创建的目录下 环境变量path下添加C:\nor…

图像采集卡 Xtium™2-XGV PX8支持高速 GigE Vision 工业相机

图像采集卡&#xff08;Image Capture Card&#xff09;&#xff0c;又称图像捕捉卡&#xff0c;是一种可以获取数字化视频图像信息&#xff0c;并将其存储和播放出来的硬件设备。很多图像采集卡能在捕捉视频信息的同时获得伴音&#xff0c;使音频部分和视频部分在数字化时同步…

python elasticsearch 日期聚合

索引以及数据如下 PUT dateagg {"mappings": {"properties": {"charge":{"type": "double"},"types":{"type": "keyword"},"create_date":{"type": "date",&…

裸机单片机适用的软件架构

单片机通常分为三种工作模式&#xff0c;分别是 1、前后台顺序执行法 2、操作系统 3、时间片轮询法 1、前后台顺序执行法 利用单片机的中断进行前后台切换&#xff0c;然后进行任务顺序执行&#xff0c;但其实在…

Spring Boot Web

目录 一. 概述 二. Spring Boot Web 1.2.1 创建SpringBoot工程&#xff08;需要联网&#xff09; 1.2.2 定义请求处理类 1.2.3 运行测试 1.3 Web分析 三. Http协议 3.1 HTTP-概述 刚才提到HTTP协议是规定了请求和响应数据的格式&#xff0c;那具体的格式是什么呢? 3…

spring结合设计模式之策略模式

策略模式基本概念&#xff1a; 一个接口或者抽象类&#xff0c;里面两个方法&#xff08;一个方法匹配类型&#xff0c;一个可替换的逻辑实现方法&#xff09;不同策略的差异化实现(就是说&#xff0c;不同策略的实现类) 使用策略模式替换判断&#xff0c;使代码更加优雅。 …

Swagger快速上手

快速开始&#xff1a; 导入maven包 <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.7.0</version> </dependency><dependency><groupId>io.springfox<…

MongoDB在Windows系统和Linux系统中实现自动定时备份

本文主要介绍MongoDB在Windows系统和Linux系统中如何实现自动定时备份。 目录 MongoDB在Windows系统中实现自动定时备份MongoDB在Linux系统中实现自动定时备份备份步骤备份恢复 MongoDB在Windows系统中实现自动定时备份 要在Windows系统中实现自动定时备份MongoDB数据库&#…

区块链实验室(32) - 下载arm64的Prysm

Prysm是Ethereum的共识层。 1. 下载prysm.sh curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod x prysm.sh2. 下载x86版prysm共识客户端 ./prysm.sh beacon-chain --download-only3.下载arm64版prysm共识客…