返回

Android Uri 转 File 失败:缺少 'file' scheme?原因与方案

Android

URI 转文件失败:缺少 'file' scheme

在 Android 应用开发中,使用 Uri 代表资源是很常见的方式,例如用户选择的文件或者应用内的一些数据。在某些情况下,你可能需要将 Uri 转换成 File 对象进行更底层的操作,像是文件读取、写入。Uri.toFile() 这种直接转换方式可能会出现 java.lang.IllegalArgumentException: Uri lacks 'file' scheme 异常。 这个异常表示当前的 Uri 不是基于本地文件系统的,转换失败。下面就这个问题的原因以及如何解决它进行一些讨论。

原因分析

问题的关键在于,并非所有的 Uri 都是指向本地文件系统资源的。 Android 中,Uri 使用不同的 "scheme" 来表示不同的资源类型,例如:

  • file:/// : 代表本地文件系统中的绝对路径。这是 Uri.toFile() 可以正常工作的情况。
  • content:/// : 通常用来表示内容提供者 (Content Provider) 返回的数据,例如相册里的图片,文件选择器选中的文档等等。
  • https:///, http:/// : 表示网络上的资源。

异常消息 Uri lacks 'file' scheme 明确地说明你当前的 Uri 的 scheme 不是 'file', 而是像是 content:///,无法直接转换为文件。当使用文件选择器选择文档,或使用 ContentResolver 从其他应用获取内容时,很大概率会遇到此类情况。此时,简单地尝试调用 .toFile() 并不可行。

解决方案:获取文件真实路径或内容流

既然 Uri 本身并非文件系统路径,那么可以有两种方式解决这个问题。第一种是尽可能获取 Uri 所指向的文件在文件系统中的真实路径,第二种方式是将Uri指向的资源读取为输入流(InputStream)。 这两种方式的适用场景略有不同,具体选择可以根据项目实际情况进行考虑。

方案一:通过 Content Resolver 获取文件路径

一些内容提供者会提供文件的真实路径,可以尝试从 Content Resolver 中查询。这种方式的局限性在于不是所有的内容提供者都暴露文件路径,而且也可能会遇到跨应用权限的问题。这种方法有一定概率解决问题,如果失败可以尝试方案二。

代码示例 (Kotlin):

import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import java.io.File
import java.io.FileOutputStream

fun uriToFile(context: Context, uri: Uri): File? {
  if (uri.scheme == "file") {
       return File(uri.path!!) // 文件 URI 直接转 File
    }

    if (uri.scheme != "content") {
      return null
    }

    var cursor = context.contentResolver.query(uri, null, null, null, null)
    cursor?.use {
      if (it.moveToFirst()) {
            val displayNameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
            val fileName = it.getString(displayNameIndex)

            val cacheDir = context.cacheDir
            val file = File(cacheDir, fileName)

            try {
               context.contentResolver.openInputStream(uri)?.use { inputStream ->
                   FileOutputStream(file).use { outputStream ->
                    val buffer = ByteArray(4 * 1024) // or any suitable size
                       var bytesRead:Int
                       while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                        outputStream.write(buffer, 0, bytesRead)
                     }
                    }

               }
               return file

          } catch(e: Exception){
              e.printStackTrace()
              return null
           }
      }
        }
    return null
}

步骤说明:

  1. 首先,检测Uri scheme 是否为 "file", 如果是直接转 File对象。
  2. 检测是否为 "content" 类型。
  3. 利用 context.contentResolver.query 查询文件信息。 其中,OpenableColumns.DISPLAY_NAME 用以获取文件的显示名,即文件名,据此建立缓存目录中的临时文件。
  4. 通过 context.contentResolver.openInputStream(uri) 获取输入流。
  5. 将输入流写入新文件。这个缓存文件,可以在用完后进行删除。

方案二:使用 InputStream 读取文件内容

如果无法直接获取文件路径, 或者想要更稳健的处理方式,则可以获取 Uri 指向资源的输入流并按需读取其内容,之后进行对应的处理,这种方式非常灵活,可以使用任意的数据读取和解析策略。

代码示例 (Kotlin):

import android.content.Context
import android.net.Uri
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader


fun readTextFromUri(context: Context, uri: Uri): String? {
   try {
    val inputStream = context.contentResolver.openInputStream(uri)
        if (inputStream != null) {
           val reader = BufferedReader(InputStreamReader(inputStream))
          val sb = StringBuilder()
        var line: String?
          while (reader.readLine().also{ line = it } != null) {
                 sb.append(line).append('\n')
          }

         return sb.toString()

         }

        }catch(e: Exception){
         e.printStackTrace()
    }
   return null
}

步骤说明:

  1. 使用 context.contentResolver.openInputStream(uri) 打开一个读取资源的 InputStream
  2. 将输入流转为字符流,读取其中的内容。

额外的安全建议

  • 处理外部提供的Uri时务必谨慎。尤其在使用 scheme 的 Uri 时, 一定要确保来源的可靠性和权限。对于方案一的文件存储过程需要谨慎,避免文件泄漏或写入敏感位置。
  • 如果文件数据涉及到敏感信息,应该对其加密或使用安全的存储方案。

以上是针对 Uri 缺少 "file" scheme 异常的常见问题解析与解决方案,选择方案一或者二可以根据项目的情况而定。总的来说,使用 InputStream 的方式通常更具有兼容性和健壮性。务必根据自己的场景选取适合自己的方法。