diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bde0fcf..00c5154 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ tools:ignore="ScopedStorage" /> Unit, - private val onCancelClick: () -> Unit ) : DialogFragment() { private lateinit var binding: DialogRenameFileBinding @@ -67,7 +68,7 @@ class RenameDialogFragment( private fun initView() { binding.etName.showKeyboard() - binding.etName.setText(pdfDocument.fileName) + binding.etName.setText(FileUtils.removeFileExtension(pdfDocument.fileName)) // 保持光标在末尾 binding.etName.setSelection(binding.etName.text?.length ?: 0) @@ -75,15 +76,28 @@ class RenameDialogFragment( private fun setupOnClick() { binding.tvCancel.setOnClickListener { - onCancelClick() dismiss() } binding.tvConfirm.setOnClickListener { val text = binding.etName.text.toString() if (validateEnter(text)) { - + val renameResult = + FileUtils.renameFile(File(pdfDocument.filePath), text) + if (renameResult.success) { + showToast(getString(R.string.rename_successfully)) + } else { + showToast(renameResult.errorMessage.toString()) + } + dismiss() } } + binding.etName.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.tilName.error = null + } + override fun afterTextChanged(s: Editable?) {} + }) } private fun validateEnter(name: String): Boolean { @@ -93,14 +107,14 @@ class RenameDialogFragment( return false } - // 名字未做修改 - if (name == pdfDocument.fileName) { + // 名字未做修改(因展示的时候去掉了扩展名,则判断的时候也去掉) + if (name == FileUtils.removeFileExtension(pdfDocument.fileName)) { binding.tilName.error = getString(R.string.name_not_changed) return false } // 含有非法字符 - val invalidChars = "[/\\\\:*?\"<>|]".toRegex() + val invalidChars = "[/\\\\:*?\"<>|.]".toRegex() if (invalidChars.containsMatchIn(name)) { binding.tilName.error = getString(R.string.name_invalid_chars) return false diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt index 85446ba..f098d6e 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt @@ -8,16 +8,34 @@ import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.provider.OpenableColumns import android.util.Log +import com.all.pdfreader.pro.app.PRApp +import com.all.pdfreader.pro.app.R import com.shockwave.pdfium.PdfiumCore import java.io.File import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.InputStream import java.security.MessageDigest import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +/** + * 文件重命名结果封装类 + */ +data class RenameResult( + val success: Boolean, + val errorMessage: String? = null +) { + companion object { + fun success() = RenameResult(true) + fun failure(message: String) = RenameResult(false, message) + } +} + object FileUtils { + fun scanPdfFiles(context: Context): List { val pdfFiles = mutableListOf() @@ -345,7 +363,8 @@ object FileUtils { try { renderer?.close() fd?.close() - } catch (_: Exception) {} + } catch (_: Exception) { + } } } @@ -364,13 +383,132 @@ object FileUtils { } } + /** + * 重命名文件(同步版本)- 智能处理后缀 + * + * @param file 要重命名的文件 + * @param newName 新文件名(可包含或不包含后缀,会自动处理) + * @return RenameResult对象,包含成功状态和错误信息 + */ + fun renameFile(file: File, newName: String): RenameResult { + if (!file.exists()) { + return RenameResult.failure(PRApp.getStringRes(R.string.error_file_not_exist)) + } + + if (!file.canWrite()) { + return RenameResult.failure(PRApp.getStringRes(R.string.error_no_write_permission)) + } + + // 智能处理后缀:如果用户没有提供后缀,保留原文件后缀 + val finalName = if (newName.contains('.')) { + // 用户提供了后缀,使用用户的 + newName + } else { + // 用户没有提供后缀,自动添加原文件后缀 + val originalExtension = file.extension + if (originalExtension.isNotEmpty()) { + "$newName.$originalExtension" + } else { + newName + } + } + + // 验证新文件名 + val validatedName = validateFileName(finalName) ?: run { + return RenameResult.failure(PRApp.getStringRes(R.string.error_invalid_file_name)) + } + + // 获取原文件的父目录 + val parentDir = file.parentFile ?: run { + return RenameResult.failure(PRApp.getStringRes(R.string.error_no_parent_directory)) + } + + // 构建新文件对象 + val newFile = File(parentDir, validatedName) + + // 检查目标文件是否已存在 + if (newFile.exists()) { + return RenameResult.failure(PRApp.getStringRes(R.string.error_target_file_exists)) + } + + return try { + if (file.renameTo(newFile)) { + Log.d("ocean", "✅ File renamed successfully: ${file.name} -> $validatedName") + RenameResult.success() + } else { + Log.e("ocean", "❌ File rename failed: ${file.path}") + RenameResult.failure(PRApp.getStringRes(R.string.error_file_rename_failed)) + } + } catch (e: SecurityException) { + RenameResult.failure(PRApp.getStringRes(R.string.error_insufficient_permission)) + } catch (e: Exception) { + Log.e("ocean", "❌ File rename exception: ${e.message}") + RenameResult.failure(PRApp.getStringRes(R.string.error_file_rename_exception, e.message.toString())) + } + } + + /** + * 重命名文件(异步版本) + * + * @param file 要重命名的文件 + * @param newName 新文件名(可包含或不包含后缀,会自动处理) + * @return RenameResult对象,包含成功状态和错误信息 + */ + suspend fun renameFileAsync(file: File, newName: String): RenameResult = + withContext(Dispatchers.IO) { + renameFile(file, newName) + } + + /** + * 移除文件扩展名(后缀) + * + * @param fileName 原始文件名 + * @return 不包含扩展名的文件名 + */ + fun removeFileExtension(fileName: String): String { + val lastDotIndex = fileName.lastIndexOf('.') + return if (lastDotIndex > 0 && lastDotIndex < fileName.length - 1) { + // 确保点号不在开头,且后面还有字符 + fileName.substring(0, lastDotIndex) + } else { + // 没有扩展名,或者点号在开头(如.hidden文件) + fileName + } + } + + /** + * 验证并清理文件名 + * + * @param fileName 原始文件名 + * @return 验证通过的文件名,如果无效则返回null + */ + fun validateFileName(fileName: String): String? { + if (fileName.isBlank()) return null + + // 移除文件系统中的非法字符 + val cleanedName = fileName.replace(Regex("[<>:\"|?*\u0000-\u001f]"), "_") + .replace(Regex("^\\.+"), "") // 移除开头的点 + .trim() + + if (cleanedName.isEmpty() || cleanedName == "." || cleanedName == "..") { + return "NewFile" + } + + // 确保文件名长度合理(通常文件系统支持255字符) + if (cleanedName.length > 255) { + return cleanedName.substring(0, 255) + } + + return cleanedName + } fun getFileFromUri(context: Context, uri: Uri): File? { // 先尝试通过 DATA 字段获取 val projection = arrayOf(MediaStore.Files.FileColumns.DATA) context.contentResolver.query(uri, projection, null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { - val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)) + val path = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)) if (!path.isNullOrEmpty()) { val file = File(path) if (file.exists()) { diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt index 2f5d762..99d23b6 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt @@ -6,7 +6,7 @@ import android.graphics.Color import android.os.ParcelFileDescriptor import android.util.Log import androidx.core.graphics.createBitmap -import com.all.pdfreader.pro.app.PDFReaderApplication +import com.all.pdfreader.pro.app.PRApp import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.repository.PdfRepository import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted @@ -76,7 +76,10 @@ class PdfScanner( LogUtil.logDebug(TAG, "异步获取图片更新数据") val newThumbnail = generateFastThumbnail(context, file) if (newThumbnail != null && doc.thumbnailPath != newThumbnail) { - pdfRepository.updateThumbnailPath(doc.filePath, newThumbnail) + pdfRepository.updateThumbnailPath( + doc.filePath, + newThumbnail + ) LogUtil.logDebug(TAG, "✅ 缩略图已更新") } } @@ -114,9 +117,7 @@ class PdfScanner( "🔄处理文件 ${index + 1}/${allFiles.size}: ${file.name} - ${file.absolutePath}" ) if (FileUtils.isPdfFile(file)) { - val existingDoc = - pdfRepository.getDocumentByPath(file.absolutePath) - + val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath) if (existingDoc == null) { LogUtil.logDebug( TAG, "🆕发现新PDF文件: ${file.name}" @@ -145,16 +146,19 @@ class PdfScanner( LogUtil.logDebug(TAG, " ✅ 已保存到数据库: ${file.name}") if (!isPassword) {//没有密码的情况下才去获取缩略图 - launch(Dispatchers.IO){ + launch(Dispatchers.IO) { val newThumbnail = generateFastThumbnail(context, file) if (newThumbnail != null && document.thumbnailPath != newThumbnail) { - pdfRepository.updateThumbnailPath(document.filePath, newThumbnail) + pdfRepository.updateThumbnailPath( + document.filePath, + newThumbnail + ) LogUtil.logDebug(TAG, "✅ 缩略图已更新") } } } } else { - LogUtil.logDebug(TAG, " 📋 文件已存在: ${file.name}") + LogUtil.logDebug(TAG, " 📋 文件已存在数据库: ${file.name}") // 🔹 文件已存在,检查是否需要更新 var needUpdate = false var updatedDoc = existingDoc.copy() @@ -192,7 +196,10 @@ class PdfScanner( LogUtil.logDebug(TAG, "异步获取图片更新数据") val newThumbnail = generateFastThumbnail(context, file) if (newThumbnail != null && existingDoc.thumbnailPath != newThumbnail) { - pdfRepository.updateThumbnailPath(existingDoc.filePath, newThumbnail) + pdfRepository.updateThumbnailPath( + existingDoc.filePath, + newThumbnail + ) LogUtil.logDebug(TAG, "✅ 缩略图已更新") } } @@ -203,16 +210,23 @@ class PdfScanner( } } } - // 打印数据库中的总记录数 + //最后过滤数据库的文件,文件不存在则删除记录,打印数据库中的总记录数 pdfRepository.getAllDocumentsOnce().forEach { doc -> - LogUtil.logDebug( - TAG, - " 📖 ${doc.fileName} - ${doc.filePath} - ${doc.pageCount}页 - ${ - FileUtils.formatFileSize( - doc.fileSize - ) - } - ${doc.thumbnailPath}" - ) + val file = File(doc.filePath) + if (!file.exists()) { + // 文件不存在 → 删除数据库记录 + LogUtil.logDebug(TAG, "最终过滤:文件不存在 -> ${doc.fileName}, 删除记录") + pdfRepository.deleteDocument(doc.filePath) + } else { + LogUtil.logDebug( + TAG, + " 📖 ${doc.fileName} - ${doc.filePath} - ${doc.pageCount}页 - ${ + FileUtils.formatFileSize( + doc.fileSize + ) + } - ${doc.thumbnailPath}" + ) + } } } // 标记扫描完成 @@ -233,7 +247,7 @@ class PdfScanner( LogUtil.logDebug( TAG, "$string 本次扫描耗时: $scannerTime ms (${scannerTime / 1000.0} 秒)" ) - PDFReaderApplication.isNeedFullScan = false + PRApp.isNeedFullScan = false withContext(Dispatchers.Main) { callback.invoke(true) } diff --git a/app/src/main/res/layout/dialog_rename_file.xml b/app/src/main/res/layout/dialog_rename_file.xml index 543b56c..81ccfc7 100644 --- a/app/src/main/res/layout/dialog_rename_file.xml +++ b/app/src/main/res/layout/dialog_rename_file.xml @@ -34,6 +34,7 @@ android:id="@+id/etName" android:layout_width="match_parent" android:layout_height="wrap_content" + android:inputType="text" android:fontFamily="@font/poppins_regular" android:textSize="16sp" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68b1574..10c6d54 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,13 @@ File name is too long (max 255 characters) A file with the same name already exists File name cannot start or end with space + Rename successfully + File does not exist + No write permission for file + Invalid file name + Cannot get parent directory + Target file already exists + File rename failed + Insufficient permission to rename file + File rename exception: %1$s \ No newline at end of file