This commit is contained in:
ocean 2025-09-10 16:29:44 +08:00
parent 369dc9d129
commit ded6be90bf
9 changed files with 227 additions and 49 deletions

View File

@ -18,7 +18,7 @@
tools:ignore="ScopedStorage" />
<application
android:name=".PDFReaderApplication"
android:name=".PRApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"

View File

@ -2,17 +2,25 @@ package com.all.pdfreader.pro.app
import android.app.Application
import android.content.Context
import androidx.annotation.StringRes
import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.util.FileChangeObserver
class PDFReaderApplication : Application() {
class PRApp : Application() {
companion object {
private lateinit var instance: PDFReaderApplication
fun getInstance(): PDFReaderApplication = instance
private lateinit var instance: PRApp
fun getInstance(): PRApp = instance
fun getContext(): Context = instance.applicationContext
//是新创建了界面则需要全盘扫描进入onResume判定扫描方法调用后置为false
var isNeedFullScan = false
fun getStringRes(@StringRes resId: Int, vararg formatArgs: Any): String {
return instance.getString(resId, *formatArgs)
}
}
private lateinit var fileChangeObserver: FileChangeObserver

View File

@ -6,7 +6,7 @@ import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.all.pdfreader.pro.app.PDFReaderApplication
import com.all.pdfreader.pro.app.PRApp
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment
@ -140,7 +140,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
logDebug("main onResume")
if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
// 有授权才初始化文件变化监听器
PDFReaderApplication.getInstance().startFileChangeObserving()
PRApp.getInstance().startFileChangeObserving()
scanningStrategy()
binding.pnLayout.visibility = View.GONE
} else {
@ -156,7 +156,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
private fun scanningStrategy() {
if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
lifecycleScope.launch {
pdfScanner.scanAndLoadPdfFiles(PDFReaderApplication.isNeedFullScan)
pdfScanner.scanAndLoadPdfFiles(PRApp.isNeedFullScan)
}
} else {
logDebug("❌ 权限不足,跳过扫描")
@ -183,7 +183,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
logDebug("main onPermissionGranted")
//授权成功后:隐藏授权提示,开始扫描文件
binding.pnLayout.visibility = View.GONE
PDFReaderApplication.getInstance().startFileChangeObserving()
PRApp.getInstance().startFileChangeObserving()
scanningStrategy()
}

View File

@ -9,17 +9,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.DialogListMoreBinding
import com.all.pdfreader.pro.app.databinding.DialogPermissionBinding
import com.all.pdfreader.pro.app.databinding.DialogSortBinding
import com.all.pdfreader.pro.app.model.SortConfig
import com.all.pdfreader.pro.app.model.SortDirection
import com.all.pdfreader.pro.app.model.SortField
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.sp.AppStore
import com.all.pdfreader.pro.app.ui.act.MainActivity.SortableFragment
import com.all.pdfreader.pro.app.util.AppUtils.dpToPx
import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation
import com.all.pdfreader.pro.app.util.FileUtils
import com.all.pdfreader.pro.app.util.FileUtils.toFormatFileSize
import com.all.pdfreader.pro.app.util.FileUtils.toSlashDate
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
@ -28,6 +22,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.launch
import java.io.File
class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment() {
@ -91,11 +86,10 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
saveCollectState(isFavorite)
}
binding.renameFileBtn.setOnClickListener {
RenameDialogFragment(pdfDocument.filePath, onOkClick = {
}, onCancelClick = {
}).show(parentFragmentManager, "ListMoreDialogFragment")
RenameDialogFragment(pdfDocument.filePath).show(
parentFragmentManager,
"ListMoreDialogFragment"
)
dismiss()
}
}

View File

@ -2,7 +2,9 @@ package com.all.pdfreader.pro.app.ui.dialog
import android.graphics.Color
import android.os.Bundle
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -15,14 +17,13 @@ import com.all.pdfreader.pro.app.databinding.DialogPdfPasswordProtectionBinding
import com.all.pdfreader.pro.app.databinding.DialogRenameFileBinding
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
import com.all.pdfreader.pro.app.util.FileUtils
import com.all.pdfreader.pro.app.util.FileUtils.isPdfPasswordCorrect
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import java.io.File
class RenameDialogFragment(
private val filePath: String,
private val onOkClick: () -> 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

View File

@ -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<File> {
val pdfFiles = mutableListOf<File>()
@ -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()) {

View File

@ -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,8 +210,14 @@ class PdfScanner(
}
}
}
// 打印数据库中的总记录数
//最后过滤数据库的文件,文件不存在则删除记录,打印数据库中的总记录数
pdfRepository.getAllDocumentsOnce().forEach { doc ->
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}页 - ${
@ -215,6 +228,7 @@ class PdfScanner(
)
}
}
}
// 标记扫描完成
ScanManager.markScanComplete(context)
val lastScanTime = ScanManager.getLastScanTime(context)
@ -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)
}

View File

@ -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" />

View File

@ -56,4 +56,13 @@
<string name="name_too_long">File name is too long (max 255 characters)</string>
<string name="name_already_exists">A file with the same name already exists</string>
<string name="name_start_end_space">File name cannot start or end with space</string>
<string name="rename_successfully">Rename successfully</string>
<string name="error_file_not_exist">File does not exist</string>
<string name="error_no_write_permission">No write permission for file</string>
<string name="error_invalid_file_name">Invalid file name</string>
<string name="error_no_parent_directory">Cannot get parent directory</string>
<string name="error_target_file_exists">Target file already exists</string>
<string name="error_file_rename_failed">File rename failed</string>
<string name="error_insufficient_permission">Insufficient permission to rename file</string>
<string name="error_file_rename_exception">File rename exception: %1$s</string>
</resources>