Android文件系统(03)MediaStore增删改查

1,447 阅读6分钟

资料

申请权限

ContextCompat.checkSelfPermission() 方法检查权限是否已授予,并使用 ActivityCompat.requestPermissions() 方法请求用户授予权限。

query()查询

至于为什么要先写查询,因为查询会涉及到字段,我们再插入数据、修改数据、删除数据的时候就需要用到一些字段。

query()函数及其入参含义

Android的ContentResolver的query方法接受五个参数,详细说明如下:

  1. Uri uri:这是查询请求的URI。它标识着要查询的数据,并且是唯一标识符。比如,如果你正在查询联系人,那么你可能使用ContactsContract.Contacts.CONTENT_URI作为你的URI。
  2. String[] projection:这是一个字符串数组,表示你希望查询哪些列。如果你不指定任何列,那么会返回所有的列。如果你想返回特定的列,你可以指定这些列的名称。
  3. String selection:这是一个可选参数,表示查询中的筛选条件。这与SQL语句中的WHERE子句类似。如果你不提供这个参数,那么所有的记录都将被返回。
  4. String[] selectionArgs:这是与selection参数一起使用的参数。它是一个字符串数组,可以替换selection中的占位符。
  5. String sortOrder:这是可选参数,表示返回记录的排序方式。这与SQL语句中的ORDER BY子句类似。如果你不提供这个参数,那么记录的排序方式将由ContentProvider决定。

简单来说,这些参数可以让你指定查询的详细要求,包括你要查询的数据、筛选条件以及返回结果的排序方式。

而且url 字段不允许为Null。

查询图片

lifecycleScope.launch {
  val cursor= requireActivity().contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,null,null,null,null)
    cursor?.let { cursor->
        while (cursor.moveToFirst()){
            LogUtils.e(cursor.columnNames)
        }
        cursor.close()
    }
}

可以看到Android 9上面可以查询到怎么多字段,但是有些字段是高版本添加的:

[_id, _data, _size, _display_name, mime_type, title, date_added, date_modified, description, picasa_id, isprivate, latitude, longitude, datetaken, orientation, mini_thumb_magic, bucket_id, bucket_display_name, width, height]

所以再性能优化上,如果说查询图片,这个入参的projection 的值需要基于业务需求获取。比如说只是需要拿到图片的uri 那么只需要传入id。回顾下上篇如何通过绝对路径获取到图片的URI:

int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
        .appendPath(String.valueOf(id))
        .build();

同样获取到图片的绝对路径:

cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
MediaStore.Images.Media 定义的常量及其含义
  1. MediaStore.Images.Media._ID:表示图像的唯一标识符,是一个整数值。
  2. MediaStore.Images.Media.DATA:表示图像的数据,是一个字符串,包含图像的路径。
  3. MediaStore.Images.Media.DISPLAY_NAME:表示图像的显示名称,是一个字符串,通常用于在 UI 中显示图像的名称。
  4. MediaStore.Images.Media.SIZE:表示图像的大小,是一个长整型值,单位为字节。
  5. MediaStore.Images.Media.MIME_TYPE:表示图像的 MIME 类型,是一个字符串,用于描述图像的格式。
  6. MediaStore.Images.Media.DATE_ADDED:表示图像被添加到设备上的日期和时间,是一个长整型值,表示自 1970 年 1 月 1 日以来的毫秒数。
  7. MediaStore.Images.Media.DATE_MODIFIED:表示图像最后修改的日期和时间,是一个长整型值,表示自 1970 年 1 月 1 日以来的毫秒数。
  8. MediaStore.Images.Media.DATE_TAKEN:表示图像拍摄的日期和时间,是一个长整型值,表示自 1970 年 1 月 1 日以来的毫秒数。
  9. MediaStore.Images.Media.ORIENTATION:表示图像的方向,是一个整数值,用于描述图像的旋转角度。

敲黑板,记得查询只需要的值。

视频

视频同理,因为MediaStore也定义了MediaStore.Video.Media.EXTERNAL_CONTENT_URI ,我们基于这个URI进行查询即可。

MediaStore.Video.Media 定义的常量及其含义
  1. MediaStore.Video.Media._ID:表示视频媒体的唯一标识符,是一个整数值。
  2. MediaStore.Video.Media.DATA:表示视频媒体的数据,是一个字符串,包含视频媒体的路径。
  3. MediaStore.Video.Media.DISPLAY_NAME:表示视频媒体的显示名称,是一个字符串,通常用于在 UI 中显示视频媒体的名称。
  4. MediaStore.Video.Media.SIZE:表示视频媒体的大小,是一个长整型值,单位为字节。
  5. MediaStore.Video.Media.MIME_TYPE:表示视频媒体的 MIME 类型,是一个字符串,用于描述视频媒体的格式。
  6. MediaStore.Video.Media.DATE_ADDED:表示视频媒体被添加到设备上的日期和时间,是一个长整型值,表示自 1970 年 1 月 1 日以来的毫秒数。
  7. MediaStore.Video.Media.DATE_MODIFIED:表示视频媒体最后修改的日期和时间,是一个长整型值,表示自 1970 年 1 月 1 日以来的毫秒数。
  8. MediaStore.Video.Media.DURATION:表示视频媒体的时长,是一个长整型值,单位为毫秒。
  9. MediaStore.Video.Media.WIDTH:表示视频媒体的宽度,是一个整数值,单位为像素。
  10. MediaStore.Video.Media.HEIGHT:表示视频媒体的高度,是一个整数值,单位为像素。

音频

音频的URI就是这个:

MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

文件

在Android 10(API级别29)之前,使用MediaStore.Files.getContentUri("external")可以访问外部存储设备上的文件。但是,从Android 10开始,由于隐私和安全性的考虑,此API已被弃用,并且无法再访问外部存储设备上的文件。所以用下面的这个:

MediaStore.Files.getContentUri("external_primary")

外部SD卡和USB驱动:

MediaStore.Files.getContentUri("external_secondary")

下载

下载目录查询的URL

MediaStore.Downloads.EXTERNAL_CONTENT_URI

总结

查询都是类似的,只是我们定义查询到字段有所差异,而且大多数字段都是定义在MediaColumns中,所以就不占字数了。获取到cursor中的值也很简单,这个和数据库查询是一致的。

insert()插入

结合上一篇SAF,可以发现创建一个文件分为两步,一步是生成一个URI,第二步是基于URI获取输入输出流进行操作,这个和低版本的直接new FIle() 是一个意思,只是低版本获取到的是绝对路径。和查询一样,也需要传入一个文件目录的URI进去,讲道理,还是应该将对应的文件添加到对于的目录的,类似于整一个非图片的文件到图片目录,这个倒是可以试试。

比如说,我们插入一张图片到相册图片存储的目录。

lifecycleScope.launch {
    val displayName = System.currentTimeMillis().toString() + ".jpg"
    val mimeType = "image/jpeg"
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)
        put(MediaStore.Images.ImageColumns.MIME_TYPE, mimeType)
        put(
            MediaStore.Images.ImageColumns.RELATIVE_PATH,
            Environment.DIRECTORY_PICTURES + "/luoye/"
        )
    }
    requireActivity().contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    )?.let {
        try {
            // 获取bitmap
            val bitmap = BitmapFactory.decodeResource(resources, R.drawable.myy)
            // 将bitmap 写入到输出流里面。
           val outputStream = requireActivity().contentResolver.openOutputStream(it)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
            outputStream?.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

在Android 9的模拟器上,设置MediaStore.Images.ImageColumns.RELATIVE_PATH 这个会无法插入成功。

那么我们插入一个pdf呢?

URI 可以生成成功,但是模拟器上没有找到这个文件。

delete() 删除

这个是这最简单的,但是需要获取到一个URI,我们知道一个URI的来源包含好几个:

  • 通过id 获取,文件绝对路径转URI也是这种思路。
  • 通过fileProvider 获取

只有通过id 获取到uri 不会华为系统检测到app 删除文件。

update() 更新

通过入参可以看到需要一个ContentValues 对象,所以说能更新的部分,也就是插入查询的部分,获取到文件流进行替换,不知道算不算更新,但是明显这个函数里面是没有体现的。更新了文件流,也需要调用一下这个函数,否则内容库可能不更新。

总结

整体来说,都是基于contentResolver 官方地址MediaStore 官网 这两个文档在进行简化描述,主要是提取出一些相关知识点。目前基于MediaStore 已经是必须适配的了。可以看到,上面主要是对函数进行了简单描述,常见的查询、排序、等等都没有阐述,这个主要想留着后面看大佬们的框架再写,否则现在写了,后面写什么,是吧。