Android:如何从内容URI获取文件URI?

187

在我的应用中,用户需要选择一个音频文件,然后应用程序会处理它。问题是,为了让应用程序对音频文件执行我想要的操作,我需要使用文件格式的URI。当我在应用中使用安卓原生音乐播放器浏览音频文件时,URI是内容URI,看起来像这样:

content://media/external/audio/media/710

然而,使用流行的文件管理器应用程序Astro,我得到了以下结果:

file:///sdcard/media/audio/ringtones/GetupGetOut.mp3

对我来说,后者更易于处理,但当然我希望应用程序能够具有与用户选择的音频文件功能相同的功能,无论他们使用什么程序浏览自己的集合。因此,我的问题是,是否有将 content:// 样式的URI转换为 file:// URI的方法?否则,您会推荐我解决这个问题的方法是什么?以下是调用选择器的代码:

Intent ringIntent = new Intent();
ringIntent.setType("audio/mp3");
ringIntent.setAction(Intent.ACTION_GET_CONTENT);
ringIntent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(Intent.createChooser(ringIntent, "Select Ringtone"), SELECT_RINGTONE);

我使用内容URI进行以下操作:

m_ringerPath = m_ringtoneUri.getPath();
File file = new File(m_ringerPath);

然后使用FileInputStream处理该文件。


1
你在使用哪些调用不支持内容URI? - Phil Lello
1
有很多内容 URI 没有文件路径,因此您无法获取文件路径。不要使用文件路径。 - Mooing Duck
11个回答

190

16
检查从选择器活动返回的URI方案。如果uri.getScheme().equals("content"),则使用内容解析器打开它。如果uri.getScheme().equals("file"),则使用常规文件方法打开它。无论哪种情况,您最终都将获得一个InputStream,可以使用通用代码进行处理。 - Jason LeBrun
25
实际上,我刚刚重新阅读了getContentResolver().openInputStream()的文档,并发现它自动适用于“content”或“file”协议,因此您不需要检查协议...如果您可以安全地假设它始终是content://或file://,那么openInputStream()将始终有效。 - Jason LeBrun
82
能否获取文件而不是InputStream(从content:...)? - AlikElzin-kilaka
4
你可以获取文件路径,但这很麻烦。请参考https://dev59.com/t2Ij5IYBdhLWcg3w04bd#20559418。 - Danyal Aytekin
12
这个答案对于那些使用依赖于文件而不是FileStream的闭源API,但仍然希望使用操作系统来允许用户选择文件的人来说是不足够的。@DanyalAytekin提到的答案正是我所需要的(事实上,我能够削减很多冗余代码,因为我知道我正在处理哪些类型的文件)。 - monkey0506
显示剩余9条评论

52

这是一个旧答案,使用了过时和不可靠的方法来解决某些特定内容解析器的痛点。如果有可能,请使用正确的openInputStream API,对这个答案要持保留态度。

您可以使用Content Resolver从content:// URI获取file://路径:

String filePath = null;
Uri _uri = data.getData();
Log.d("","URI = "+ _uri);                                       
if (_uri != null && "content".equals(_uri.getScheme())) {
    Cursor cursor = this.getContentResolver().query(_uri, new String[] { android.provider.MediaStore.Images.ImageColumns.DATA }, null, null, null);
    cursor.moveToFirst();   
    filePath = cursor.getString(0);
    cursor.close();
} else {
    filePath = _uri.getPath();
}
Log.d("","Chosen path = "+ filePath);

1
谢谢,这个完美地解决了我的问题。我不能像被接受的答案建议的那样使用InputStream。 - ldam
5
仅适用于本地文件,例如不适用于谷歌云盘。 - paulgavrikov
1
有时候可以工作,有时候会返回 file:///storage/emulated/0/... 这个不存在的文件路径。 - Reza Mohammadi
1
如果方案是 content://,那么列 "_data" (android.provider.MediaStore.Images.ImageColumns.DATA) 是否总是存在? - Edward Falk
4
这是一个重要的反模式。有些ContentProvider提供了该列,但当您尝试绕过ContentResolver时,并不能保证您具有读/写访问File。请使用ContentResolver方法来操作content:// uri,这是官方推荐的方法,也受到Google工程师的鼓励。 - user1643723
显示剩余6条评论

26

尝试这个...

从内容 URI 获取文件

fun fileFromContentUri(context: Context, contentUri: Uri): File {
    // Preparing Temp file name
    val fileExtension = getFileExtension(context, contentUri)
    val fileName = "temp_file" + if (fileExtension != null) ".$fileExtension" else ""

    // Creating Temp file
    val tempFile = File(context.cacheDir, fileName)
    tempFile.createNewFile()

    try {
        val oStream = FileOutputStream(tempFile)
        val inputStream = context.contentResolver.openInputStream(contentUri)

        inputStream?.let {
            copy(inputStream, oStream)
        }

        oStream.flush()
    } catch (e: Exception) {
        e.printStackTrace()
    }

    return tempFile
}

private fun getFileExtension(context: Context, uri: Uri): String? {
    val fileType: String? = context.contentResolver.getType(uri)
    return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType)
}

@Throws(IOException::class)
private fun copy(source: InputStream, target: OutputStream) {
    val buf = ByteArray(8192)
    var length: Int
    while (source.read(buf).also { length = it } > 0) {
        target.write(buf, 0, length)
    }
}

你的回答真的帮了我很多,谢谢你,谢谢 <3 - Dr.KeyOk

14

如果您有一个内容Uri,其格式为content://com.externalstorage...,您可以使用此方法在Android 19或以上版本中获取文件夹或文件的绝对路径。

public static String getPath(final Context context, final Uri uri) {
    final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

    // DocumentProvider
    if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
        System.out.println("getPath() uri: " + uri.toString());
        System.out.println("getPath() uri authority: " + uri.getAuthority());
        System.out.println("getPath() uri path: " + uri.getPath());

        // ExternalStorageProvider
        if ("com.android.externalstorage.documents".equals(uri.getAuthority())) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];
            System.out.println("getPath() docId: " + docId + ", split: " + split.length + ", type: " + type);

            // This is for checking Main Memory
            if ("primary".equalsIgnoreCase(type)) {
                if (split.length > 1) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1] + "/";
                } else {
                    return Environment.getExternalStorageDirectory() + "/";
                }
                // This is for checking SD Card
            } else {
                return "storage" + "/" + docId.replace(":", "/");
            }

        }
    }
    return null;
}

你可以使用 println 检查 Uri 的每个部分。下面列出了我的 SD 卡和设备主存储器的返回值。如果文件在存储器中,您可以访问并删除它,但我无法使用此方法从 SD 卡中删除文件,只能使用此绝对路径读取或打开图像。如果您找到使用此方法删除的解决方案,请分享。

SD CARD

getPath() uri: content://com.android.externalstorage.documents/tree/612E-B7BF%3A/document/612E-B7BF%3A
getPath() uri authority: com.android.externalstorage.documents
getPath() uri path: /tree/612E-B7BF:/document/612E-B7BF:
getPath() docId: 612E-B7BF:, split: 1, type: 612E-B7BF

主存储器

getPath() uri: content://com.android.externalstorage.documents/tree/primary%3A/document/primary%3A
getPath() uri authority: com.android.externalstorage.documents
getPath() uri path: /tree/primary:/document/primary:
getPath() docId: primary:, split: 1, type: primary

如果您希望在获取路径后使用file:///来获取Uri,请使用以下方法:

DocumentFile documentFile = DocumentFile.fromFile(new File(path));
documentFile.getUri() // will return a Uri with file Uri

1
我认为这不是在应用程序中正确的做法。可悲的是,我正在一个快速项目中使用它。 - Bondax
@Bondax 是的,您应该使用内容URI而不是文件路径或文件URI。这就是引入存储访问框架的原因。但是,如果您希望获取文件URI,则使用DocumentsContract类是其他答案中最正确的方法。如果您查看GitHub上的Google示例,您会发现它们也使用此类来获取文件夹的子文件夹。 - Thracian
DocumentFile类也是API 19中的新添加项,您如何使用SAF中的URI。正确的方法是为您的应用程序使用标准路径,并要求用户通过SAF UI授予文件夹权限,将Uri字符串保存到共享首选项中,当需要访问文件夹时使用DocumentFile对象。 - Thracian

14

灵感来自Jason LaBrunDarth Raven的回答。尝试已经回答过的方法,我找到了下面的解决方案,它可能大多数情况下涵盖了光标空值情况和从content://转换为file://

要进行文件转换,请从获取的uri中读取并写入文件。

public static Uri getFilePathFromUri(Uri uri) throws IOException {
    String fileName = getFileName(uri);
    File file = new File(myContext.getExternalCacheDir(), fileName);
    file.createNewFile();
    try (OutputStream outputStream = new FileOutputStream(file);
         InputStream inputStream = myContext.getContentResolver().openInputStream(uri)) {
        FileUtil.copyStream(inputStream, outputStream); //Simply reads input to output stream
        outputStream.flush();
    }
    return Uri.fromFile(file);
}

要获取文件名,它将覆盖光标为空的情况。

public static String getFileName(Uri uri) {
    String fileName = getFileNameFromCursor(uri);
    if (fileName == null) {
        String fileExtension = getFileExtension(uri);
        fileName = "temp_file" + (fileExtension != null ? "." + fileExtension : "");
    } else if (!fileName.contains(".")) {
        String fileExtension = getFileExtension(uri);
        fileName = fileName + "." + fileExtension;
    }
    return fileName;
}

有一个很好的选项可以将MIME类型转换为文件扩展名

 public static String getFileExtension(Uri uri) {
    String fileType = myContext.getContentResolver().getType(uri);
    return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
}

获取文件名的光标

public static String getFileNameFromCursor(Uri uri) {
    Cursor fileCursor = myContext.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null);
    String fileName = null;
    if (fileCursor != null && fileCursor.moveToFirst()) {
        int cIndex = fileCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
        if (cIndex != -1) {
            fileName = fileCursor.getString(cIndex);
        }
    }
    return fileName;
}

2
谢谢,我已经看了一个星期了。虽然不喜欢为此复制文件,但它确实有效。 - justdan0227

9
尝试通过调用ContentResolver.query()来处理content://方案的URI不是一个好的解决方案。在运行4.2.2的HTC Desire上,您可能会得到NULL作为查询结果。
为什么不使用ContentResolver呢? https://dev59.com/wmXWa4cB1Zd3GeqPLEXh#29141800

但有时我们只需要路径。我们不一定要将文件加载到内存中。 - Kimi Chiu
4
如果您没有访问权限,那么“路径”就毫无用处。例如,如果应用程序向您提供与其私有内部目录中的文件对应的content:// uri,则您将无法在新的Android版本中使用File APIs来使用该uri。 ContentResolver的设计是为了克服这种安全限制。如果您从ContentResolver获取了uri,则可以期望它能正常工作。 - user1643723

4

抱歉回复晚了,但我的代码已经测试过了

从URI检查方案:

 byte[] videoBytes;

if (uri.getScheme().equals("content")){
        InputStream iStream =   context.getContentResolver().openInputStream(uri);
            videoBytes = getBytes(iStream);
        }else{
            File file = new File(uri.getPath());
            FileInputStream fileInputStream = new FileInputStream(file);     
            videoBytes = getBytes(fileInputStream);
        }

在上述答案中,我将视频URI转换为字节数组,但这与问题无关。我只是复制了我的全部代码以展示FileInputStreamInputStream的用法,在我的代码中两者都能正常工作。
我在Fragment中使用了变量context,它是getActivity(),在Activity中,它直接是ActivityName.this。 context=getActivity(); //在Fragment中 context=ActivityName.this; //在Activity中

我知道这与问题无关,但是你会如何使用 byte[] videoBytes;?大多数答案只展示了如何使用 InputStream 处理图像。 - KRK

1
您可以使用以下的Android包,这可能会更容易一些。

https://github.com/Blankj/AndroidUtilCode

使用上述包,代码可以如下所示:

导入以下行

import com.blankj.utilcode.util.UriUtils;

您的代码可以如下所示:

File f = UriUtils.uri2File(result);

谢谢


0

你可以使用这个函数在新的Android和旧版本中从URI获取文件。

fun getFileFromUri(context: Context, uri: Uri?): File? {
    uri ?: return null
    uri.path ?: return null

    var newUriString = uri.toString()
    newUriString = newUriString.replace(
        "content://com.android.providers.downloads.documents/",
        "content://com.android.providers.media.documents/"
    )
    newUriString = newUriString.replace(
        "/msf%3A", "/image%3A"
    )
    val newUri = Uri.parse(newUriString)

    var realPath = String()
    val databaseUri: Uri
    val selection: String?
    val selectionArgs: Array<String>?
    if (newUri.path?.contains("/document/image:") == true) {
        databaseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        selection = "_id=?"
        selectionArgs = arrayOf(DocumentsContract.getDocumentId(newUri).split(":")[1])
    } else {
        databaseUri = newUri
        selection = null
        selectionArgs = null
    }
    try {
        val column = "_data"
        val projection = arrayOf(column)
        val cursor = context.contentResolver.query(
            databaseUri,
            projection,
            selection,
            selectionArgs,
            null
        )
        cursor?.let {
            if (it.moveToFirst()) {
                val columnIndex = cursor.getColumnIndexOrThrow(column)
                realPath = cursor.getString(columnIndex)
            }
            cursor.close()
        }
    } catch (e: Exception) {
        Log.i("GetFileUri Exception:", e.message ?: "")
    }
    val path = realPath.ifEmpty {
        when {
            newUri.path?.contains("/document/raw:") == true -> newUri.path?.replace(
                "/document/raw:",
                ""
            )
            newUri.path?.contains("/document/primary:") == true -> newUri.path?.replace(
                "/document/primary:",
                "/storage/emulated/0/"
            )
            else -> return null
        }
    }
    return if (path.isNullOrEmpty()) null else File(path)
}

0
如果您绝对确定您的content: Uri指向一个真实的文件,可以使用以下技巧:
context.contentResolver.openFileDescriptor(uri, "r").use { fileDescriptor ->
    if (fileDescriptor == null) {
        throw NullPointerException("ParcelFileDescriptor from $uri was null")
    }
    val path = "/proc/${android.os.Process.myPid()}/fd/${fileDescriptor.fd}"
    val fileUri = Uri.fromFile(File(path).canonicalFile)
}

在这里,我们使用ContentResolver打开文件,构建底层Linux文件描述符的路径,并通过获取canonicalFile来解析实际文件路径。


无法与三星手机配合使用,似乎他们限制了这些通话。java.lang.SecurityException: MediaProvider: 用户11518对文件:///proc/8285/fd/25没有读取权限 - TheD3t0
无法与三星手机配合使用,似乎他们限制了这些通话。java.lang.SecurityException: MediaProvider: 用户11518对文件:///proc/8285/fd/25没有读取权限 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接