Android ActivityResultContracts.GetContent 真实的文件路径

1,695 阅读7分钟

如何获取手机文件的真实路径?

一、通过Result API获取手机中的单个文件

这个比较简单直接贴代码,以获取手机中stl格式的文件为例:

注册

private lateinit var registerFile: ActivityResultLauncher<String>

registerFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
    if (uri != null) {

    }
}

使用

//获取stl格式的文件
registerFile.launch("application/vnd.ms-pki.stl")

二、通过Uri获取真实的文件名

获取Uri后往往需要展示文件名,文件名应该如何获取?

registerFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
    if (uri != null) {
        val cursor = requireActivity().contentResolver.query(uri, null, null, null, null)
        val index = cursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        cursor?.moveToFirst()
        //真实文件名
        val fileName = cursor?.getString(index!!)
    }
}

三、不同Android版本获取文件路径

在Android 29以前获取文件路径相对比较简单,这里主要讨论Android 29以后获取文件路径。Android 29以后Android更新了分区存储的机制,我们其实是拿不到文件的真实路径的,上传文件等都会报错,于是就有了沙盒机制,原理就是通过IO流把文件复制到应用存储目录下cache文件夹,代码如下:

var file: File? = null
//android10以上转换
if (uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
    file = File(uri.path)
} else if (uri.scheme.equals(ContentResolver.SCHEME_CONTENT)) {
    //把文件复制到沙盒目录
    val contentResolver: ContentResolver = context.contentResolver
    val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
    try {
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                //通过Cursor获取真实的文件名
                val displayName: String = cursor.getString(
                    cursor.getColumnIndex(
                        OpenableColumns.DISPLAY_NAME
                    )
                )

                val fileSize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE))
                //判断存储空间是否足够
                if (getRemainingStorageSpace() <= fileSize) {
                    showToast(R.string.not_enough_storage_space)
                    return null
                }

                val inputStream: InputStream? = contentResolver.openInputStream(uri)
                //在沙盒中存储也是用的真实的文件名
                val pathFile = File(context.getExternalFilesDir(null), dirName)
                if (!pathFile.exists()) {
                    pathFile.mkdirs()
                }
                val cache = File(pathFile, displayName)
                val fos = FileOutputStream(cache)
                if (inputStream != null) {
                    FileUtils.copy(inputStream, fos)
                }
                file = cache
                fos.close()
                inputStream?.close()
            }
        }
    } catch (e: Exception) {
        e.printStackTrace();
    } finally {
        cursor?.close()
    }
}
return file?.absolutePath

四、沙盒存储存在的问题

通过以上可以知道,在Android 29及以上文件会被通过沙盒机制复制到应用存储目录下cache文件夹,于是你可爱的测试同事开始上传二个同名的大文件,这时候cache文件夹肯定不会出现二个同名的文件,而是会互相覆盖,于是你的上传文件就会报错了。解决办法也很简单,复制到cache目录的文件必须不同名,于是需要手动的生成cache目录下的文件名,比如加上一串UUID:

/**
 * 获取22位长度的UUID
 */
object UUIDHelper {
    private val DIGITS64 =
        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()

    /**
     * 获取22位长度的UUID
     */
    fun get22UUID(): String {
        val u = UUID.randomUUID()
        return toIDString(u.mostSignificantBits) + toIDString(u.leastSignificantBits)
    }

    private fun toIDString(l: Long): String {
        var l = l
        val buf = "00000000000".toCharArray() // 限定11位长度
        var length = 11
        val least = 61L // 0x0000003FL
        do {
            buf[--length] = DIGITS64[(l and least).toInt()] // l & least取低6位
            /* 无符号的移位只有右移,没有左移
             * 使用“>>>”进行移位
             * 为什么没有无符号的左移呢,知道原理的说一下哈
             */l = l ushr 6
        } while (l != 0L)
        return String(buf)
    }
}

加上UUID后的代码

var file: File? = null
//android10以上转换
if (uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
    file = File(uri.path)
} else if (uri.scheme.equals(ContentResolver.SCHEME_CONTENT)) {
    //把文件复制到沙盒目录
    val contentResolver: ContentResolver = context.contentResolver
    val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
    try {
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                //通过Cursor获取真实的文件名
                val displayName: String = cursor.getString(
                    cursor.getColumnIndex(
                        OpenableColumns.DISPLAY_NAME
                    )
                )

                val fileSize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE))
                //判断存储空间是否足够
                if (getRemainingStorageSpace() <= fileSize) {
                    showToast(R.string.not_enough_storage_space)
                    return null
                }

                val inputStream: InputStream? = contentResolver.openInputStream(uri)
                //在沙盒中存储用uuid加上真实的文件名,用"-"区分,用"-"切割后也能拿到真实的文件名
                val pathFile = File(context.getExternalFilesDir(null), "${UUIDHelper.get22UUID}"-"$displayName")
                if (!pathFile.exists()) {
                    pathFile.mkdirs()
                }
                
                val fos = FileOutputStream(pathFile)
                if (inputStream != null) {
                    FileUtils.copy(inputStream, fos)
                }
                file = cache
                fos.close()
                inputStream?.close()
            }
        }
    } catch (e: Exception) {
        e.printStackTrace();
    } finally {
        cursor?.close()
    }
}
return file?.absolutePath

另外沙盒中的文件用完一定要记得删除,因为是copy过来的,是实实在在的文件,但是低于Android Q的不能删除,需要特别注意

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    val file = File(filePath)
    if (file.exists()) file.delete()
}

如果想自定义沙盒内的文件名也是可以的,传个文件名进来就可以了,自由发挥。

五、完整的代码

优化后完整代码如下:

import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.FileUtils
import android.os.StatFs
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.text.TextUtils
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream

object FileProviderKtUtils {

    //文件夹的名称
    private const val dirName = "UploadFile"

    /**
     * 根据Uri获取文件绝对路径,解决Android4.4以上版本Uri转换 兼容Android 10
     *
     * @param context
     * @param imageUri
     * @param fileName 自定义沙盒的文件名,避免沙盒中同名的情况,另外要注意的一点是沙盒中的文件记得要删除
     */
    fun getFileAbsolutePath(context: Context, imageUri: Uri?, fileName: String = ""): String? {
        if (imageUri == null) {
            return null
        }

        //低于Android 19
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            return getRealFilePath(context, imageUri)
        }

        //大于等于Android 19,小于Android 29
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
            && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
            && DocumentsContract.isDocumentUri(context, imageUri)
        ) {
            if (isExternalStorageDocument(imageUri)) {
                val docId: String = DocumentsContract.getDocumentId(imageUri)
                val split = docId.split(":")
                val type = split[0]
                if ("primary" == type) {
                    return "${Environment.getExternalStorageDirectory()}/${split[1]}"
                }
            } else if (isDownloadsDocument(imageUri)) {
                val id = DocumentsContract.getDocumentId(imageUri)
                return if (!TextUtils.isEmpty(id)) {
                    if (id.startsWith("raw:")) {
                        id.replaceFirst("raw:", "")
                    } else {
                        try {
                            getContentPath(context, id)
                        } catch (exception: NumberFormatException) {
                            null
                        }
                    }
                } else {
                    null
                }
            } else if (isMediaDocument(imageUri)) {
                val docId = DocumentsContract.getDocumentId(imageUri)
                val split = docId.split(":")
                val type = split[0]
                var contentUri: Uri? = null
                when (type) {
                    "image" -> {
                        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                    }

                    "video" -> {
                        contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
                    }

                    "audio" -> {
                        contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
                    }

                    else -> {}
                }
                val selection = "${MediaStore.Images.Media._ID}=?"
                val selectionArgs = arrayOf(split[1])
                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }

        // MediaStore (and general)
        //大于Android 29
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return if (fileName.isNotEmpty()) {
                uriToFileApiQCustomFileName(context, imageUri, fileName)
            } else {
                uriToFileApiQ(context, imageUri)
            }
        } else if ("content" == imageUri.scheme) {
            // Return the remote address
            if (isGooglePhotosUri(imageUri)) {
                return imageUri.lastPathSegment;
            }
            return getDataColumn(context, imageUri, null, null);
        }
        // File
        else if ("file" == imageUri.scheme) {
            return imageUri.path
        }
        return null
    }

    /**
     * 获取download文件夹存储内容的路径
     */
    private fun getContentPath(context: Context, id: String): String? {
        val contentUriPrefixesToTry = arrayOf(
            "content://downloads/public_downloads",
            "content://downloads/my_downloads",
            "content://downloads/all_downloads"
        )

        for (contentUriPrefix in contentUriPrefixesToTry) {
            val contentUri =
                ContentUris.withAppendedId(Uri.parse(contentUriPrefix), id.toLong())
            try {
                val path = getDataColumn(context, contentUri, null, null)
                if (path != null) {
                    return path
                }
            } catch (_: java.lang.Exception) {

            }
        }
        return null
    }


    //此方法 只能用于4.4以下的版本
    private fun getRealFilePath(context: Context, uri: Uri): String? {
        val scheme = uri.scheme;
        var data: String? = null
        if (scheme == null) {
            data = uri.path
        } else if (ContentResolver.SCHEME_FILE == scheme) {
            data = uri.path;
        } else if (ContentResolver.SCHEME_CONTENT == scheme) {
            val projection = arrayOf(MediaStore.Images.ImageColumns.DATA)
            val cursor: Cursor? = context.contentResolver.query(
                uri,
                projection,
                null,
                null,
                null
            );

//            Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null);
            if (null != cursor) {
                if (cursor.moveToFirst()) {
                    val index: Int = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
                    if (index > -1) {
                        data = cursor.getString(index);
                    }
                }
                cursor.close();
            }
        }
        return data;
    }


    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    private fun isExternalStorageDocument(uri: Uri): Boolean {
        return "com.android.externalstorage.documents" == uri.authority
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    private fun isDownloadsDocument(uri: Uri): Boolean {
        return "com.android.providers.downloads.documents" == uri.authority
    }

    private fun getDataColumn(
        context: Context,
        uri: Uri?,
        selection: String?,
        selectionArgs: Array<String>?
    ): String? {
        var cursor: Cursor? = null
        val column: String = MediaStore.Images.Media.DATA
        val projection = arrayOf(column)
        if (uri != null) {
            try {
                cursor =
                    context.contentResolver.query(uri, projection, selection, selectionArgs, null)
                if (cursor != null && cursor.moveToFirst()) {
                    val index: Int = cursor.getColumnIndexOrThrow(column)
                    return cursor.getString(index);
                }
            } catch (exception: IllegalArgumentException) {
                return getFilePathFromURI(context, uri)
            } finally {
                cursor?.close()
            }
        }
        return null
    }

    /**
     * 获取文件路径
     */
    private fun getFilePathFromURI(context: Context, uri: Uri): String? {
        val fileName = getFileName(uri)
        if (!TextUtils.isEmpty(fileName)) {
            val copyFile = File(context.cacheDir, fileName)
            copyCacheFile(context, uri, copyFile)
            return copyFile.absolutePath
        }
        return null
    }

    /**
     * 获取文件名
     */
    private fun getFileName(uri: Uri?): String? {
        if (uri == null) return null
        var fileName: String? = null
        uri.path?.let { path ->
            path.lastIndexOf('/').also {
                if (it != -1) {
                    fileName = path.substring(it + 1)
                }
            }
        }
        return fileName
    }

    /**
     * 复制文件
     */
    private fun copyCacheFile(context: Context, uri: Uri, copyFile: File) {
        try {
            val inputStream = context.contentResolver.openInputStream(uri)
            val outputStream = FileOutputStream(copyFile)
            val buffer = ByteArray(1024)
            var length: Int

            if (inputStream != null) {
                while (inputStream.read(buffer).also { length = it } > 0) {
                    outputStream.write(buffer, 0, length)
                }
                outputStream.flush()
                inputStream.close()
                outputStream.close()
            }
        } catch (exception: IOException) {

        }
    }


    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    private fun isMediaDocument(uri: Uri): Boolean {
        return "com.android.providers.media.documents" == uri.authority
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is Google Photos.
     */
    private fun isGooglePhotosUri(uri: Uri): Boolean {
        return "com.google.android.apps.photos.content" == uri.authority
    }


    /**
     * Android 10 以上适配 另一种写法
     * @param context
     * @param uri
     * @return
     */
    @SuppressLint("Range")
    private fun getFileFromContentUri(context: Context, uri: Uri): String? {
        if (uri == null) {
            return null
        }
        val filePath: String?
        val filePathColumn =
            arrayOf(MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME)
        val contentResolver: ContentResolver = context.contentResolver
        val cursor: Cursor? = contentResolver.query(
            uri, filePathColumn, null,
            null, null
        )
        if (cursor != null) {
            cursor.moveToFirst()
            try {
                filePath = cursor.getString(cursor.getColumnIndex(filePathColumn[0]))
                return filePath
            } catch (e: Exception) {
            } finally {
                cursor.close()
            }
        }
        return ""
    }

    /**
     * Android 10 以上适配
     * @param context
     * @param uri
     * @return
     */
    @SuppressLint("Range")
    @RequiresApi(api = Build.VERSION_CODES.Q)
    private fun uriToFileApiQ(context: Context, uri: Uri): String? {
        var file: File? = null
        //android10以上转换
        if (uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
            file = File(uri.path)
        } else if (uri.scheme.equals(ContentResolver.SCHEME_CONTENT)) {
            val contentResolver: ContentResolver = context.contentResolver
            val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
            try {
                //把文件复制到沙盒目录
                if (cursor != null) {
                    if (cursor.moveToFirst()) {
                        //通过Cursor获取真实的文件名
                        val displayName: String = cursor.getString(
                            cursor.getColumnIndex(
                                OpenableColumns.DISPLAY_NAME
                            )
                        )
                        val fileSize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE))
                        //判断存储空间是否足够
                        if (getRemainingStorageSpace() <= fileSize) {
                            showToast(R.string.not_enough_storage_space)
                            return null
                        }

                        val inputStream: InputStream? = contentResolver.openInputStream(uri)
                        //在沙盒中存储也是用的真实的文件名
                        val pathFile = File(context.getExternalFilesDir(null), dirName)
                        if (!pathFile.exists()) {
                            pathFile.mkdir()
                        }
                        val cache = File(pathFile, displayName)
                        val fos = FileOutputStream(cache)
                        if (inputStream != null) {
                            FileUtils.copy(inputStream, fos)
                        }
                        file = cache
                        fos.close()
                        inputStream?.close()
                    }

                }
            } catch (e: Exception) {
                e.printStackTrace();
            } finally {
                cursor?.close()
            }

        }
        return file?.absolutePath
    }

    /**
     * Android 10 以上适配
     * @param context
     * @param uri
     * @return
     */
    @SuppressLint("Range")
    @RequiresApi(api = Build.VERSION_CODES.Q)
    private fun uriToFileApiQCustomFileName(context: Context, uri: Uri, fileName: String): String? {
        var file: File? = null
        //android10以上转换
        if (uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
            file = File(uri.path)
        } else if (uri.scheme.equals(ContentResolver.SCHEME_CONTENT)) {
            //把文件复制到沙盒目录
            val contentResolver: ContentResolver = context.contentResolver
            val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
            try {
                if (cursor != null) {
                    if (cursor.moveToFirst()) {
                        val fileSize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE))
                        //判断存储空间是否足够
                        if (getRemainingStorageSpace() <= fileSize) {
                            showToast(R.string.not_enough_storage_space)
                            return null
                        }

                        val inputStream: InputStream? = contentResolver.openInputStream(uri)
                        val pathFile = File(context.getExternalFilesDir(null), dirName)
                        if (!pathFile.exists()) {
                            pathFile.mkdirs()
                        }
                        val cache = File(pathFile, fileName)
                        val fos = FileOutputStream(cache)
                        if (inputStream != null) {
                            FileUtils.copy(inputStream, fos)
                        }
                        file = cache
                        fos.close()
                        inputStream?.close()
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                cursor?.close()
            }
        }
        return file?.absolutePath
    }


    /**
     * path转uri
     * @param context
     * @param filePath
     * @return
     */
    fun toUri(context: Context, filePath: String): Uri {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return FileProvider.getUriForFile(
                context,
                context.applicationInfo.packageName + ".fileprovider",
                File(filePath)
            )
        }
        return Uri.fromFile(File(filePath))
    }

    /**
     * 获取剩余的存储空间
     */
    private fun getRemainingStorageSpace(): Long {
        kotlin.runCatching {
            val iPath: File = Environment.getDataDirectory()
            val iStat = StatFs(iPath.path)
            val iBlockSize = iStat.blockSizeLong
            val iAvailableBlocks = iStat.availableBlocksLong
            iAvailableBlocks * iBlockSize
        }.onSuccess {
            return it
        }.onFailure {
            return 0
        }
        return 0
    }

}

另外记得在AndroidManifest配置provider

<!--添加Provider-->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="你应用的包名.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true"
    tools:replace="android:authorities">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/main_file_paths" />
</provider>

xml文件代码

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <root-path
            name="root"
            path="" />
        <files-path
            name="files"
            path="" />

        <cache-path
            name="cache"
            path="" />

        <external-path
            name="external"
            path="" />

        <external-path
            name="files_root"
            path="Android/data/你的包名/" />

        <external-files-path
            name="external_file_path"
            path="" />

        <external-cache-path
            name="external_cache_path"
            path="" />
    </paths>
</resources>

总结

这篇博客主要是连续选择并上传大文件到腾讯云、亚马逊云遇到的坑,可能里面会有一些不足,欢迎大家一起交流!