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
错误原因分析
- 文件名获取逻辑漏洞:
getFileName方法在无法从Uri中读取到文件名时,会返回空字符串。此时File(cacheDir, "")会直接指向cacheDir目录本身,而非具体文件。 - 无效的流写入操作:尝试对目录创建
FileOutputStream时,系统会抛出FileNotFoundException,因为不能向目录写入数据流。 - 冗余操作与空指针风险:原代码存在两次重复的文件拷贝,且大量使用非空断言
!!,容易引发额外崩溃。
修复方案
修改后的完整代码
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" }
关键修改点说明
- 修复文件名获取逻辑:在
getFileName中增加多层判断,当无法获取到有效文件名时,返回默认值unknown_video.mp4,彻底避免空字符串。 - 空安全优化:用
runCatching、getOrNull、takeIf替代非空断言!!,减少崩溃风险。 - 合并冗余操作:将两次文件拷贝合并为一次,直接将Uri流写入缓存目录的目标文件,提升性能。
- 增加边界检查:添加数组越界、文件有效性等检查,覆盖更多异常场景。
内容的提问来源于stack exchange,提问作者Mohammad TellFa
相关产品推荐
相关产品推荐

