You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android屏幕录制上传服务器遇FileNotFoundException错误求解决

修复屏幕录制视频上传时的FileNotFoundException错误

我用HBRecorder实现屏幕录制功能,录制完成后需要将视频上传至服务器,但运行代码时出现java.io.FileNotFoundException: /data/user/0/com.myapp/cache (Is a directory)错误,相关代码及错误日志如下:

原代码

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityCompressUploadBinding.inflate(layoutInflater)
        setContentView(binding.root)
        //File uri
        lifecycleScope.launch {
            videoData.getVideoPath.collect { storedPath ->
                if (storedPath.isNullOrEmpty()) {
                    fileUri = if (SDK_INT >= VERSION_CODES.Q) FILE_URI else FILE_PATH?.convertFileToUri()
                    testId = TEST_ID
                    testTitle = TEST_TITLE
                    //videoData.saveVideoPath(fileUri.toString())
                } else {
                    fileUri = Uri.parse(storedPath)
                    videoData.getVideoInfo.collect { info ->
                        if (info.isNullOrEmpty().not()) {
                            val infoSplit = info?.split(SPLITTER)!!
                            testId = infoSplit[0].toInt()
                            testTitle = "${infoSplit[1]} $testId"
                        }
                    }
                }
            }
        }

        lifecycleScope.launch {
            delay(300)
            val inputStream: InputStream = contentResolver.openInputStream(fileUri!!)!!
            val file = File.createTempFile("video", ".mp4")
            inputStream.use { input ->
                val outputStream = FileOutputStream(file)
                input.copyTo(outputStream)
            }

            fileUri?.let { uri ->
                val parcelFile = contentResolver.openFileDescriptor(uri, "r", null)
                val inputStream = FileInputStream(parcelFile!!.fileDescriptor)

                val file = File(cacheDir, contentResolver.getFileName(uri) ?: "temp_video.mp4")

                val outputStream = FileOutputStream(file)
                inputStream.copyTo(outputStream)
                outputStream.close()
                inputStream.close()

                if (file.exists() && file.length() > 0) {
                    val fileName = URLEncoder.encode(file.name, "UTF-8")
                    val progressRequest = ProgressRequestBody(file, "multipart/form-data") { progress ->
                        runOnUiThread {
                            binding.apply {
                                uploadProgress.progress = progress
                                uploadPercentageTxt.text = "$progress%"
                            }
                        }
                    }

                    val filePart = MultipartBody.Part.createFormData("file", fileName, progressRequest)
                    bodyUpload.file = filePart

                    videoData.saveVideoPath(uri.toString())
                    viewModel.callPostUploadVideoNew(testId, bodyUpload)

                    Log.e("FilePathLog", "File: ${file.path} --- Name : $fileName")
                } else {
                    Log.e("FileError", "File was not created or is empty")
                }
            }

        }
    }
private fun ContentResolver.getFileName(uri: Uri): String { 
   var name = ""
   val returnCursor = query(uri, null, null, null, null)
   if (returnCursor != null) {
      val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
      returnCursor.moveToFirst()    
      name = returnCursor.getString(nameIndex)
      returnCursor.close()
   } 
   return name
}

错误日志

java.io.FileNotFoundException: /data/user/0/com.myapp/cache (Is a directory)
at java.io.FileOutputStream.open0(Native Method)
at java.io.FileOutputStream.open(FileOutputStream.java:308)
at java.io.FileOutputStream.<init>(FileOutputStream.java:238)
at java.io.FileOutputStream.<init>(FileOutputStream.java:180)
at com.testady.ui.compress_upload.CompressUploadActivity$onCreate$2.invokeSuspend(CompressUploadActivity.kt:167)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:235)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:168)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:595)
at kotlinx.coroutines.android.HandlerContext$scheduleResumeAfterDelay$$inlined$Runnable$1.run(Runnable.kt:19)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7050)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:965)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException

错误原因分析

  1. 文件名获取逻辑漏洞:getFileName方法在无法从Uri中读取到文件名时,会返回空字符串。此时File(cacheDir, "")会直接指向cacheDir目录本身,而非具体文件。
  2. 无效的流写入操作:尝试对目录创建FileOutputStream时,系统会抛出FileNotFoundException,因为不能向目录写入数据流。
  3. 冗余操作与空指针风险:原代码存在两次重复的文件拷贝,且大量使用非空断言!!,容易引发额外崩溃。

修复方案

修改后的完整代码

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    _binding = ActivityCompressUploadBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // 监听视频路径数据
    lifecycleScope.launch {
        videoData.getVideoPath.collect { storedPath ->
            if (storedPath.isNullOrEmpty()) {
                fileUri = if (SDK_INT >= VERSION_CODES.Q) FILE_URI else FILE_PATH?.convertFileToUri()
                testId = TEST_ID
                testTitle = TEST_TITLE
            } else {
                fileUri = Uri.parse(storedPath)
                videoData.getVideoInfo.collect { info ->
                    info?.takeIf { it.isNotEmpty() }?.let { validInfo ->
                        val infoSplit = validInfo.split(SPLITTER)
                        if (infoSplit.size >= 2) { // 增加数组越界检查
                            testId = infoSplit[0].toIntOrNull() ?: TEST_ID // 安全转换
                            testTitle = "${infoSplit[1]} $testId"
                        }
                    }
                }
            }
        }
    }

    // 处理文件上传
    lifecycleScope.launch {
        delay(300)
        val targetUri = fileUri ?: return@launch // 空安全处理,直接返回避免崩溃

        // 从Uri获取输入流
        val inputStream = runCatching { contentResolver.openInputStream(targetUri) }.getOrNull()
            ?: run {
                Log.e("FileError", "无法打开Uri输入流")
                return@launch
            }

        // 创建目标文件:确保文件名有效,避免指向目录
        val fileName = contentResolver.getFileName(targetUri)
        val targetFile = File(cacheDir, fileName)

        // 拷贝文件到本地缓存
        runCatching {
            inputStream.use { input ->
                FileOutputStream(targetFile).use { output ->
                    input.copyTo(output)
                }
            }
        }.onFailure {
            Log.e("FileError", "文件拷贝失败", it)
            return@launch
        }

        // 检查文件是否有效
        if (!targetFile.exists() || targetFile.length() <= 0) {
            Log.e("FileError", "文件未创建或为空")
            return@launch
        }

        // 准备上传请求
        val encodedFileName = URLEncoder.encode(targetFile.name, "UTF-8")
        val progressRequest = ProgressRequestBody(targetFile, "multipart/form-data") { progress ->
            runOnUiThread {
                binding.apply {
                    uploadProgress.progress = progress
                    uploadPercentageTxt.text = "$progress%"
                }
            }
        }

        val filePart = MultipartBody.Part.createFormData("file", encodedFileName, progressRequest)
        bodyUpload.file = filePart

        videoData.saveVideoPath(targetUri.toString())
        viewModel.callPostUploadVideoNew(testId, bodyUpload)

        Log.e("FilePathLog", "File: ${targetFile.path} --- Name : $encodedFileName")
    }
}

// 修复后的文件名获取方法:确保返回有效文件名
private fun ContentResolver.getFileName(uri: Uri): String {
    val returnCursor = query(uri, null, null, null, null)
    returnCursor?.use { cursor ->
        val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        if (nameIndex != -1 && cursor.moveToFirst()) {
            cursor.getString(nameIndex).takeIf { it.isNotEmpty() } ?: "unknown_video.mp4"
        } else {
            "unknown_video.mp4"
        }
    } ?: "unknown_video.mp4"
}

关键修改点说明

  1. 修复文件名获取逻辑:在getFileName中增加多层判断,当无法获取到有效文件名时,返回默认值unknown_video.mp4,彻底避免空字符串。
  2. 空安全优化:用runCatching、getOrNull、takeIf替代非空断言!!,减少崩溃风险。
  3. 合并冗余操作:将两次文件拷贝合并为一次,直接将Uri流写入缓存目录的目标文件,提升性能。
  4. 增加边界检查:添加数组越界、文件有效性等检查,覆盖更多异常场景。

内容的提问来源于stack exchange,提问作者Mohammad TellFa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 08:15:23