Scoped Storage技术问题:Documents目录日志文件重复创建及查询返回空求助
问题排查与修复方案
核心问题分析
1. 文件匹配失败导致重复创建
createNewFileAndWrite方法中设置DISPLAY_NAME时未添加.txt后缀,但查询时却判断文件名包含.txt。MediaStore不会自动为无后缀文件补充后缀,导致查询时无法匹配到已创建的文件,每次都会触发新文件创建,系统为避免重名自动生成(1)、(2)这类后缀的文件。
2. 查询条件不严谨引发Cursor异常
- 仅匹配路径未过滤文件类型,会把目录下的文件夹、其他类型文件全部查出,增加匹配难度
- 直接使用
cursor!!强制非空,当contentResolver.query返回null时会抛出空指针,导致后续逻辑中断 - 路径格式不统一:代码中
FOLDER以/开头和结尾,拼接后路径带有冗余斜杠,与MediaStore实际存储的RELATIVE_PATH格式不匹配
3. 写入模式错误无法实现追加
使用"rwt"模式会清空原有内容再写入,若要实现追加需改用"wa"模式。
修复后的完整代码
const val FOLDER = "Test/TestDocuments" // 去掉首尾斜杠,统一路径格式 const val FILE_NAME = "Log" fun Context.createFolder() { try { val values = ContentValues().apply { put( MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOCUMENTS}/$FOLDER" ) put(MediaStore.MediaColumns.IS_DIRECTORY, true) // 明确标记为目录 } val uri = contentResolver.insert( MediaStore.Files.getContentUri("external"), values ) Toast.makeText(this, "目录创建成功: $uri", Toast.LENGTH_SHORT).show() } catch (e: Exception) { Toast.makeText(this, "目录创建失败: ${e.message}", Toast.LENGTH_SHORT).show() } } fun Context.findAndAppendToFile() { val contentUri = MediaStore.Files.getContentUri("external") val targetFileName = "$FILE_NAME${getPreferenceData(LOGGER_UNIQUE_ID)}.txt" val targetPath = "${Environment.DIRECTORY_DOCUMENTS}/$FOLDER/" // 精确查询:匹配路径+文件名+排除目录 val selection = """ ${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${MediaStore.MediaColumns.IS_DIRECTORY} = ? """.trimIndent() val selectionArgs = arrayOf(targetPath, targetFileName, "0") contentResolver.query(contentUri, null, selection, selectionArgs, null)?.use { cursor -> var uri: Uri? = null if (cursor.moveToFirst()) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) uri = ContentUris.withAppendedId(contentUri, id) } if (uri == null) { Toast.makeText(this, "未找到目标日志文件,创建新文件", Toast.LENGTH_SHORT).show() createNewFileAndWrite() } else { try { contentResolver.openOutputStream(uri, "wa")?.use { outputStream -> outputStream.write("追加的日志内容\n".toByteArray()) } Toast.makeText(this, "日志追加成功", Toast.LENGTH_SHORT).show() } catch (e: IOException) { Toast.makeText(this, "日志写入失败: ${e.message}", Toast.LENGTH_SHORT).show() } } } ?: run { Toast.makeText(this, "查询文件失败,尝试创建新文件", Toast.LENGTH_SHORT).show() createNewFileAndWrite() } } fun Context.createNewFileAndWrite() { val targetFileName = "$FILE_NAME${getPreferenceData(LOGGER_UNIQUE_ID)}.txt" val values = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName) // 直接带.txt后缀 put(MediaStore.MediaColumns.MIME_TYPE, "text/plain") put( MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOCUMENTS}/$FOLDER/" ) put(MediaStore.MediaColumns.IS_DIRECTORY, false) // 明确标记为文件 } contentResolver.insert(MediaStore.Files.getContentUri("external"), values)?.let { uri -> try { contentResolver.openOutputStream(uri)?.use { outputStream -> outputStream.write("初始日志内容\n".toByteArray()) } Toast.makeText(this, "新日志文件创建成功: $uri", Toast.LENGTH_SHORT).show() } catch (e: IOException) { Toast.makeText(this, "新文件写入失败: ${e.message}", Toast.LENGTH_SHORT).show() } } ?: run { Toast.makeText(this, "新文件创建失败", Toast.LENGTH_SHORT).show() } } // 示例:getPreferenceData实现(确保返回当日唯一标识) fun Context.getPreferenceData(key: String): String { val prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE) val today = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date()) prefs.edit().putString(key, today).apply() return prefs.getString(key, today) ?: today }
关键修复点说明
- 路径格式统一:将
FOLDER改为不带首尾斜杠的格式,拼接时用/连接,确保与MediaStore存储的RELATIVE_PATH格式一致 - 文件名一致性:创建文件时直接指定带
.txt的文件名,查询时用完全相同的名称匹配,避免后缀不匹配问题 - 精确查询条件:添加
IS_DIRECTORY = 0过滤文件,同时匹配路径和文件名,减少无效结果;使用getColumnIndexOrThrow避免列名错误 - 安全资源处理:使用
use函数自动关闭Cursor和OutputStream,避免资源泄漏;处理query返回null的异常情况 - 追加模式设置:使用
"wa"模式打开输出流,实现内容追加而非覆盖 - 目录创建修正:添加
IS_DIRECTORY = true明确标记为目录,确保创建的是文件夹而非文件
内容的提问来源于stack exchange,提问作者vishnuc156
相关产品推荐
相关产品推荐

