美文网首页
MediaStore使用实战:实现“0权限”操作下载目录等

MediaStore使用实战:实现“0权限”操作下载目录等

作者: BlueSocks | 来源:发表于2024-01-08 22:06 被阅读0次

    前言

    • 在以前,操作外部存储通常的做法是申请存储权限,然后通过文件路径取得File(path),进行相关操作。随着谷歌对相关权限的收紧,Android 10引入分区存储Android 11强制执行,加上国内要求targetAPI必须提升到30+,存储操作需要做新的适配。

    • 由于谷歌提供的文档较为简略,所以以操作Download目录为例撰写这篇文章。谷歌文档中提及的不止这一种,包括如下:

      图片(包括照片和屏幕截图),存储在 DCIM/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Images 表格中。

      视频,存储在 DCIM/、Movies/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Video 表格中。

      音频文件,存储在 Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/ 目录中。此外,系统还可以识别 Music/ 或 Movies/ 目录中的音频播放列表,以及 Recordings/ 目录中的录音。系统将这些文件添加到 MediaStore.Audio 表格中。Recordings/ 目录在 Android 11(API 级别 30)及更低版本中不可用。

      下载的文件,存储在 Download/ 目录中。在搭载 Android 10(API 级别 29)及更高版本的设备上,这些文件存储在 MediaStore.Downloads 表格中。此表格在 Android 9(API 级别 28)及更低版本中不可用。

    先说结论

    • 可以通过设置requestLegacyExternalStorage标记并将targetSdkVersion保持在29,申请MANAGE_EXTERNAL_STORAGE权限等办法,延续以前的代码,但这是非常不文雅的做法,强烈不推荐。
    • MediaStore是谷歌推荐解决方案之一,还有保存到专属文件目录、使用SAF等方案
    • MediaStore API只在Android 10及以上提供,低版本系统还需要保留兼容代码。本文主要讲Android 10+的做法,低版本代码略去。
    • 使用MediaStore的本质是通过指定参数用ContentResolver查询数据库,获得所需文件的Uri,然后进行操作。

    开始使用

    权限

    • 可能用到的几条权限:
      <!-- 需要访问其他应用创建的图片时申请 -->
      <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    
      <!-- 需要访问其他应用创建的视频时申请 -->
      <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    
      <!-- 需要访问其他应用创建的音频时申请 -->
      <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    
      <!-- 旧版安卓兼容 -->
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    
      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                       android:maxSdkVersion="29" />
    
    
    • 以我的项目为例,只需要访问本应用存放到下载目录的文件,因此对于Android 10+,不需要申请任何权限,对于旧版本,保留读写权限。最终配置如下:
      <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
      <uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
    
    

    通过文件路径取得文件Uri

    • 这里的文件路径均以Environment.DIRECTORY_DOWNLOADS开头,也就是“Download”,且包含文件名
          /**
           * 通过MediaStore获取文件uri
           * @return 获取失败返回null
           */
          @RequiresApi(Build.VERSION_CODES.Q)
          fun getFileUri(context: Context, path: String): Uri? {
            // projection代表数据库中需要检索出来的列,也可以不写,query的第二个参数传null,写了性能更好
              val projection = arrayOf(
                  MediaStore.Downloads.DISPLAY_NAME,
                  MediaStore.Downloads._ID,
                  MediaStore.Downloads.RELATIVE_PATH
              )
              // 从path解析出路径和文件名
              val directoryPath = path.substringBeforeLast("/")
              val fileName = path.substringAfterLast("/")
      
            // SQL语句,路径匹配和文件名匹配
              val selection =
                  "${MediaStore.Downloads.RELATIVE_PATH} LIKE ? AND ${MediaStore.Downloads.DISPLAY_NAME} = ?"
              // SQL语句参数
              val selectionArgs = arrayOf("%$directoryPath%", fileName)
      
              val contentResolver: ContentResolver = context.contentResolver
              val uri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
            // 使用ContentResolver查找,获得数据库指针
              val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null)
      
              var fileUri: Uri? = null
              if (cursor?.moveToFirst() == true) {
                  val columnIndex = cursor.getColumnIndex(MediaStore.Downloads._ID)
                  val fileId = cursor.getLong(columnIndex)
                  fileUri = Uri.withAppendedPath(uri, fileId.toString())
                  cursor.close()
              }
              return fileUri
          }
      
    
    

    向Download写文件

    • 这里的inputStream可以通过File#inputStream()获得,也可以是其他形式的inputStream,如okhttp下载文件。提供灵活性。
    • path支持直接嵌套目录。
        /**
         * 写inputStream到公共目录Download
         * @param path 文件路径,必须以Download/开头,且不包含文件名
         */
        @RequiresApi(Build.VERSION_CODES.Q)
        fun writeToDownload(
            context: Context,
            path: String,
            fileName: String,
            inputStream: InputStream
        ) {
            val contentValues = ContentValues()
            // 设置文件路径
            contentValues.put(MediaStore.Downloads.RELATIVE_PATH, path)
            // 设置文件名称
            contentValues.put(MediaStore.Downloads.DISPLAY_NAME, fileName)
            // ContentUri 表示操作哪个数据库, contentValues 表示要插入的数据内容
            val uri = context.contentResolver.insert(
                MediaStore.Downloads.EXTERNAL_CONTENT_URI,
                contentValues
            )!!
            // 向 path/filename 文件中插入数据
            val os: OutputStream = context.contentResolver.openOutputStream(uri)!!
            val bos = BufferedOutputStream(os)
            inputStream.use { istream ->
                bos.use { bos ->
                    val buff = ByteArray(1024)
                    var count: Int
                    while (istream.read(buff).apply { count = this } != -1) {
                        bos.write(buff, 0, count)
                    }
                }
            }
        }
    
    
    

    通过path检查文件存在性

        fun checkFileExistence(context: Context, path: String): Boolean {
            return getFileUri(context, path) != null
        }
    
    

    通过Uri获取文件名

        fun getFileName(context: Context, uri: Uri): String {
            var fileName = ""
            val contentResolver = context.contentResolver
            // 此处只需要取出文件名这一项
            val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
            contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
                val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                cursor.moveToFirst()
                fileName = cursor.getString(nameIndex)
                cursor.close()
            }
    
            return fileName
        }
    
    

    删除指定path文件

        fun deleteFile(context: Context, path: String) {
            val uri = getFileUri(context, path)
            if (uri == null) {
                return
            }
            context.contentResolver.delete(uri, null, null)
        }
    
    

    跳转其他程序打开文件

        fun openFile(context: Context, path: String) {
            val intent = Intent()
            intent.action = Intent.ACTION_VIEW
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            val uri = getFileUri(context, path)
            if (uri == null) {
                toastError("打开文件失败")
                return
            }
            intent.setDataAndType(uri, context.contentResolver.getType(uri) ?: "*/*")
            try {
                context.startActivity(intent)
            } catch (exception: ActivityNotFoundException) {
                // 对于设定的MIME没有对应程序可打开的情况
                intent.setDataAndType(uri, "*/*")
                context.startActivity(intent)
            }
        }
    
    

    附:判断是否可以使用MediaStore的一个小函数

        private fun useMediaStore(path: String): Boolean {
            return path.startsWith(Environment.DIRECTORY_DOWNLOADS) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
        }
    
    

    后记

    • 这些操作都是针对文件的,MediaStore似乎没有提供针对目录的操作。
    • 本文根据个人理解撰写,如有不同意见,欢迎讨论。

    相关文章

      网友评论

          本文标题:MediaStore使用实战:实现“0权限”操作下载目录等

          本文链接:https://www.haomeiwen.com/subject/cbsondtx.html