Android MediaStore Api 使用
本文是对 关于 Android 的文件存储目录的补充
在 Android Q 后,获得 External Storage 的权限后 使用 Environment.getExternalStorageDirectory 和 File Api 对外置存储中的文件进行操作 这种方式已经不被允许了,需要开发者进行适配,后续开发者需要通过 Storage Access Framework 或者 MediaStore 的 Api 来对 External Storage 中的文件进行操作
关于权限
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
通过 MediaStore Api 访问应用自身存放到公共目录下的文件不需要申请权限,而如果要访问其他应用保存到公共目录下的文件则需要申请权限
关于 MimeType
从 com.android.media.MediaFormat
源码中我们可以找到 Android 定义好的一些 MineType
例如:
创建/保存文件
构造一个 ContentValues 对象,通过 ContentResolver.insert 插入到对应的目录中,该方法会返回一个 Uri,通过对该 Uri 进行文件流写入即可
示例:
1 | private fun saveImage(bitmap: Bitmap){ |
注意:
- ContentValues 其实是内部使用了一个 ArrayMap 的数据结构用来存放数据,所以我们可以根据我们需要保存的文件信息,给 ContentValues 设置对应的值
例如:
1 | val values = ContentValues().apply { |
具体举几个例子,可见下面的表格
key | value |
---|---|
mime_type | 设置文件的 MimeType |
_display_name | 指定保存的文件名,如果不设置,则系统会取当前的时间戳作为文件名 |
relative_path | 指定保存的文件目录,例如上文我们将这个图片保存到了 Pictures/DemoPicture 文件夹下,如果不设置这个值,则会被默认保存到对应的媒体类型的文件夹下,例如,图片文件(mimeType = image/*)会被保存到 Pictures(Environment#DIRECTORY_PICTURES) 中,需要注意的是,不能将文件放置到不对应的顶级文件夹下,比如将一个 mimeType 为 audio/mpeg 放大 Pictures 这样的行为是不被允许的,也就是如果设置 MIME_TYPE = audia/* 并将 RELATIVE_PATH 设置为 Environment#DIRECTORY_PICTURES 这样是会 Throw IllegalArgumentException 的 |
例如:
1 | val values = ContentValues().apply { |
结果是:
1 | Caused by: java.lang.IllegalArgumentException: Primary directory Video not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures] |
Android 外部存储中的标准存储文件目录如下:
1 | sdcard |
前面一列为 MimeType ,后一列为其对应的 Primary Directory
—— 表示未知
需要注意的是,对于 Android 中的媒体类型,如果是需要提供给其他应用使用的,在卸载后仍需保留的媒体文件,按照规范,应当放到对应的公共目录媒体文件夹下
MimeType | 对应文件夹 |
---|---|
图片(image/*) | DCIM,Pictures |
音频(audio/*) | Alarms, Music, Notifications, Podcasts, Ringtones |
视频(video/*) | Movies |
文档(file/*) | Documents,Download |
当然,这些也都可以通过 MediaStore 放到 Downloads
文件夹下
- ContentValues 的 key 值可以通过
MediaStore.XXX.Media.YYY
获取到
XXX: 对应的媒体类型
YYY: 对应的字段常量 - RELATIVE_PATH 的 String 值不需要以
/
开头 - insert(uri: Uri,value: ContentValues) 的第一个参数可以通过 MediaStore 中的常量获取,具体如下
获取 insert 方法中的第一个入参的方式
1 | * Images |
疑问 1
有个疑问是前文图标中标记为 ------
未知的地方,根据官方文档,Audiobooks 是可以存放 audio/*
类型的文件的,但通过以下代码插入一个 audio/*
类型的文件却抛出异常了
1 | val values = ContentValues().apply { |
其实就是 Android 会帮我们将这些媒体数据存放到一个数据库中,这些我们设置的数据都是数据库表中的字段,除了上面表格中罗列的一些常用的信息,还有很多数据可以设置,详细可以参考 android.media.MediaStore.MediaColumns
类,在这个类中我们发现了一个 owner_package_name
的字段,这个字段的作用,我们后面再说
删除自己应用创建的文件
同 SAF ,获取到 Uri 后即可通过contentResolver.delete(uri,null,null)
删除即可
查询自己应用的文件
通过
Cursor query(@RequiresPermission.Read @NonNull Uri uri,@Nullable String[] projection, @Nullable String selection,@Nullable String[] selectionArgs, @Nullable String sortOrder)
方法
参数解释:
参数 | 类型 | 释义 |
---|---|---|
uri | Uri | 提供检索内容的 Uri,其 scheme 是content:// |
projection | String[] | 返回的列,如果传递 null 则所有列都返回(效率低下) |
selection | String | 过滤条件,即 SQL 中的 WHERE 语句(但不需要写 where 本身),如果传 null 则返回所有的数据 |
selectionArgs | String[] | 如果你在 selection 的参数加了 ? 则会被本字段中的数据按顺序替换掉 |
sortOrder | String | 用来对数据进行排序,即 SQL 语句中的 ORDER BY (单不需要写ORDER BY 本身),如果传 null 则按照默认顺序排序(可能是无序的) |
举个🌰
1 | private fun getImages(): List<Uri>{ |
访问其他 App 的文件
不知道大家有没有看到上面的标题写的都是「自己应用」的文件,这是因为我们的 App 至今还未申请 READ_EXTERNAL_STORAGE
和 WRITE_EXTERNAL_STORAGE
权限,通过 MediaStore Api 对自己应用创建的文件,是不需要权限的。这时因为在创建的时候系统会将我们的应用的 packageName
写入 owner_package_name
字段从而在后续的使用中判断这个文件是哪个应用创建的。
那如果应用需要访问或者修改其他应用的文件怎么办呢。
如果只是要读取,则申请
READ_EXTERNAL_STORAGE
权限后即可通过 MediaStore Api 进行读取例如我们上述的
查询自己应用的文件
中的查询语句,如果申请了读取外置存储的权限后,返回的数据就会包含了其他 App 提供给 Media 的图片了
如下图:
没有读取权限时:
获得读取权限后:
多出来几张其他 App 产生的图片
如果需要编辑修改甚至删除其他应用的文件,则需要申请
WRITE_EXTERNAL_STORAGE
权限。如果当应用没有
WRITE_EXTERNAL_STORAGE
权限时,去修改其他 App 的文件时,则会 throwjava.lang.SecurityException: xxxx has no access to content://media/external/images/media/243
的异常当应用拥有了
WRITE_EXTERNAL_STORAGE
权限后,当修改其他 App 的文件时,会 throw 另一个 Exceptionandroid.app.RecoverableSecurityException: xxxxxx has no access to content://media/external/images/media/243
如果我们将这个 RecoverableSecurityException 给 Catch 住,并向用户申请修改该图片的权限,用户操作后,我们就可以在 onActivityResult 回调中拿到结果进行操作了
1 | try { |
如下图,用户会收到这样的提示框。
PS. 当用户授权后,我们对该文件进行修改后,后续对这个文件的修改就不再会抛出 RecoverableSecurityException
了
Android MediaStore Api 使用
https://ppting.me/2020/04/19/2020_04_19_how_to_use_Android_MediaStore_Api/