优化扫描方法:

1.分快速扫描与全盘扫描
2.下拉刷新必定调用全扫
3.每次重新创建应用则进行全盘扫描
4.每次只进入onResume则进行快速扫描
This commit is contained in:
ocean 2025-09-08 11:53:20 +08:00
parent 810399bcf8
commit 1f664cd191
6 changed files with 252 additions and 147 deletions

View File

@ -11,6 +11,8 @@ class PDFReaderApplication : Application() {
private lateinit var instance: PDFReaderApplication private lateinit var instance: PDFReaderApplication
fun getInstance(): PDFReaderApplication = instance fun getInstance(): PDFReaderApplication = instance
fun getContext(): Context = instance.applicationContext fun getContext(): Context = instance.applicationContext
//是新创建了界面则需要全盘扫描进入onResume判定扫描方法调用后置为false
var isNeedFullScan = false
} }
private lateinit var fileChangeObserver: FileChangeObserver private lateinit var fileChangeObserver: FileChangeObserver
@ -18,7 +20,7 @@ class PDFReaderApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
isNeedFullScan = true
// 初始化文件变化监听(不立即启动) // 初始化文件变化监听(不立即启动)
fileChangeObserver = FileChangeObserver(this) fileChangeObserver = FileChangeObserver(this)

View File

@ -158,20 +158,28 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
} }
private fun scanningStrategy() { private fun scanningStrategy() {
// 智能扫描策略
if (pdfScanner.shouldScan()) {
logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)")
if (StoragePermissionHelper.hasBasicStoragePermission(this)) { if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
lifecycleScope.launch { lifecycleScope.launch {
pdfScanner.scanAndLoadPdfFiles() pdfScanner.scanAndLoadPdfFiles(PDFReaderApplication.isNeedFullScan)
} }
} else { } else {
logDebug("❌ 权限不足,跳过扫描") logDebug("❌ 权限不足,跳过扫描")
} }
} else {
val hoursAgo = pdfScanner.getHoursSinceLastScan() // // 智能扫描策略
logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前") // if (pdfScanner.shouldScan()) {
} // logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)")
// if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
// lifecycleScope.launch {
// pdfScanner.scanAndLoadPdfFiles()
// }
// } else {
// logDebug("❌ 权限不足,跳过扫描")
// }
// } else {
// val hoursAgo = pdfScanner.getHoursSinceLastScan()
// logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前")
// }
} }
// 授权后续操作 // 授权后续操作

View File

@ -52,7 +52,7 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
lifecycleScope.launch { lifecycleScope.launch {
val pdfScanner = PdfScanner(requireContext(), getRepository()) val pdfScanner = PdfScanner(requireContext(), getRepository())
pdfScanner.scanAndLoadPdfFiles { b -> pdfScanner.scanAndLoadPdfFiles(true) { b ->
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
} }
} }

View File

@ -8,11 +8,14 @@ import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.util.FileUtils.getFileFromUri
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
class FileChangeObserver( class FileChangeObserver(
private val context: Context, private val context: Context,
@ -32,7 +35,15 @@ class FileChangeObserver(
contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) { override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri) super.onChange(selfChange, uri)
handleFileChange() if (uri != null) {
val file = getFileFromUri(context, uri)
if (file != null) {
Log.d("ocean", "修改的文件: ${file.absolutePath}")
// 可以传给你的增量扫描逻辑
handleFileChange(file)
}
}
} }
} }
@ -63,7 +74,7 @@ class FileChangeObserver(
} }
} }
private fun handleFileChange() { private fun handleFileChange(file: File) {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastScanTime < debounceDelay) { if (currentTime - lastScanTime < debounceDelay) {
return // 防抖 return // 防抖
@ -76,7 +87,18 @@ class FileChangeObserver(
if (StoragePermissionHelper.hasBasicStoragePermission(context)) { if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
Log.d("ocean", "📂 检测到文件变化,可以执行增量扫描...") Log.d("ocean", "📂 检测到文件变化,可以执行增量扫描...")
val pdfRepository = PdfRepository.getInstance()
val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath)
if (existingDoc != null) {
LogUtil.logDebug(
"ocean",
"PdfScanner: 📖 ${existingDoc.fileName} - ${existingDoc.pageCount}页 - ${
FileUtils.formatFileSize(
existingDoc.fileSize
)
} - ${existingDoc.thumbnailPath}"
)
}
} }
} }
} }

View File

@ -344,4 +344,35 @@ object FileUtils {
} catch (_: Exception) {} } catch (_: Exception) {}
} }
} }
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))
if (!path.isNullOrEmpty()) {
val file = File(path)
if (file.exists()) {
return file
}
}
}
}
// Android 10+ 或 DATA 字段为空时,使用 ContentResolver 流访问
return try {
val tempFile = File(context.cacheDir, "temp_pdf_${System.currentTimeMillis()}.pdf")
context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
tempFile
} catch (e: Exception) {
Log.e("ocean", "无法获取文件: ${e.message}", e)
null
}
}
} }

View File

@ -8,6 +8,7 @@ import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import com.all.pdfreader.pro.app.PDFReaderApplication
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.room.repository.PdfRepository import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted
@ -23,70 +24,99 @@ class PdfScanner(
private val TAG = "ocean-PdfScanner" private val TAG = "ocean-PdfScanner"
suspend fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) { suspend fun scanAndLoadPdfFiles(isNeedFullScan: Boolean, callback: (Boolean) -> Unit = {}) {
if (!StoragePermissionHelper.hasBasicStoragePermission(context)) { if (!StoragePermissionHelper.hasBasicStoragePermission(context)) {
LogUtil.logDebug(TAG, "PdfScanner: 权限不足") LogUtil.logDebug(TAG, "权限不足")
callback.invoke(false) callback.invoke(false)
return return
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
LogUtil.logDebug(TAG, "PdfScanner: 🔍 开始扫描PDF文件...") val startScannerTime = System.currentTimeMillis()
LogUtil.logDebug(TAG, "🔍开始扫描PDF文件...")
LogUtil.logDebug(TAG, "获取数据库数据")
val cachedDocs = pdfRepository.getAllDocumentsOnce()
var needFullScan = isNeedFullScan
if (!needFullScan) {
cachedDocs.forEach { doc ->
val file = File(doc.filePath)//得到数据库存储的文件路径
if (file.exists()) {
LogUtil.logDebug(TAG, "$file 文件存在,检测是否存在变化")
var needUpdate = false
var updatedDoc = doc.copy()
// 修改时间 与 文件大小
if (file.lastModified() != doc.lastModified || file.length() != doc.fileSize) {
LogUtil.logDebug(TAG, "文件时间或者大小被修改 -> ${doc.fileName}")
updatedDoc = updatedDoc.copy(
lastModified = file.lastModified(),
fileSize = file.length()
)
needUpdate = true
}
if (needUpdate) {
// 是否加密
val currentIsPassword = isPdfEncrypted(file)
if (doc.isPassword != currentIsPassword) {
LogUtil.logDebug(TAG, "密码状态变化 -> ${doc.fileName}")
updatedDoc = updatedDoc.copy(isPassword = currentIsPassword)
}
// 缩略图(仅非加密文件)
if (!currentIsPassword) {
val newThumbnail = generateThumbnail(context, file)
if (doc.thumbnailPath != newThumbnail) {
updatedDoc = updatedDoc.copy(thumbnailPath = newThumbnail)
}
} else if (doc.thumbnailPath != null) {
updatedDoc = updatedDoc.copy(thumbnailPath = null)
}
pdfRepository.insertOrUpdateDocument(updatedDoc)
LogUtil.logDebug(TAG, "✅数据库已更新: ${doc.fileName}")
}
} else {
// 文件不存在 → 删除数据库记录,并触发全盘扫描
LogUtil.logDebug(TAG, "文件不存在 -> ${doc.fileName}, 删除记录")
pdfRepository.deleteDocument(doc.fileHash)
needFullScan = true
}
}
}
if (needFullScan || cachedDocs.isEmpty()) {
LogUtil.logDebug(TAG, "数据库不完整,执行全盘扫描...")
// 扫描应用私有目录(无需权限) // 扫描应用私有目录(无需权限)
val privateFiles = FileUtils.scanPdfFiles(context) val privateFiles = FileUtils.scanPdfFiles(context)
LogUtil.logDebug( LogUtil.logDebug(TAG, "📁应用私有目录找到: ${privateFiles.size} 个PDF文件")
TAG, "PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件"
)
privateFiles.forEach { file ->
LogUtil.logDebug(
TAG,
"PdfScanner: 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})"
)
}
// 扫描MediaStore需要权限 // 扫描MediaStore需要权限
val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(context) val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(context)
LogUtil.logDebug( LogUtil.logDebug(TAG, "📱MediaStore找到: ${mediaStoreFiles.size} 个PDF文件")
TAG, "PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件"
)
mediaStoreFiles.forEach { file ->
LogUtil.logDebug(
TAG,
"PdfScanner: 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})"
)
}
// 合并并去重 // 合并并去重
val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath } val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath }
LogUtil.logDebug(TAG, "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件") LogUtil.logDebug(TAG, "📊总计扫描到: ${allFiles.size} 个PDF文件")
// 处理每个PDF文件 // 处理每个PDF文件
allFiles.forEachIndexed { index, file -> allFiles.forEachIndexed { index, file ->
LogUtil.logDebug( LogUtil.logDebug(
TAG, TAG,
"PdfScanner: 🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name} - ${file.absolutePath}" "🔄处理文件 ${index + 1}/${allFiles.size}: ${file.name} - ${file.absolutePath}"
) )
if (FileUtils.isPdfFile(file)) { if (FileUtils.isPdfFile(file)) {
val fileHash = FileUtils.calculateFileHash(file.absolutePath) val fileHash = FileUtils.calculateFileHash(file.absolutePath)
LogUtil.logDebug(TAG, "PdfScanner: 🔑 文件哈希: $fileHash") LogUtil.logDebug(TAG, "🔑文件哈希: $fileHash")
if (fileHash != null) { if (fileHash != null) {
val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath) val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath)
if (existingDoc == null) { if (existingDoc == null) {
LogUtil.logDebug( LogUtil.logDebug(
TAG, "PdfScanner: 🆕 发现新PDF文件: ${file.name}" TAG, "🆕发现新PDF文件: ${file.name}"
) )
var thumbnailPath: String? = null var thumbnailPath: String? = null
val isPassword = isPdfEncrypted(file) val isPassword = isPdfEncrypted(file)
LogUtil.logDebug(TAG, "PdfScanner: isPassword->${isPassword}") LogUtil.logDebug(TAG, "isPassword->${isPassword}")
if (!isPassword) {//没有密码的情况下才去获取缩略图 if (!isPassword) {//没有密码的情况下才去获取缩略图
thumbnailPath = generateThumbnail(context, file) ?: "" thumbnailPath = generateThumbnail(context, file) ?: ""
} }
LogUtil.logDebug(TAG, "PdfScanner: thumbnailPath->${thumbnailPath}") LogUtil.logDebug(TAG, "thumbnailPath->${thumbnailPath}")
val metadata = val metadata =
PdfMetadataExtractor.extractMetadata(file.absolutePath) PdfMetadataExtractor.extractMetadata(file.absolutePath)
val document = PdfDocumentEntity( val document = PdfDocumentEntity(
@ -107,17 +137,17 @@ class PdfScanner(
) )
pdfRepository.insertOrUpdateDocument(document) pdfRepository.insertOrUpdateDocument(document)
LogUtil.logDebug( LogUtil.logDebug(
TAG, "PdfScanner: ✅ 已保存到数据库: ${file.name}" TAG, " ✅ 已保存到数据库: ${file.name}"
) )
} else { } else {
LogUtil.logDebug(TAG, "PdfScanner: 📋 文件已存在: ${file.name}") LogUtil.logDebug(TAG, " 📋 文件已存在: ${file.name}")
// 🔹 文件已存在,检查是否需要更新 // 🔹 文件已存在,检查是否需要更新
var needUpdate = false var needUpdate = false
var updatedDoc = existingDoc.copy() var updatedDoc = existingDoc.copy()
// 路径/修改时间更新 // 路径/修改时间更新
if (existingDoc.filePath != file.absolutePath || existingDoc.lastModified != file.lastModified()) { if (existingDoc.filePath != file.absolutePath || existingDoc.lastModified != file.lastModified()) {
LogUtil.logDebug(TAG, "PdfScanner: ✅ 路径/修改时间需要更新") LogUtil.logDebug(TAG, "✅ 路径/修改时间需要更新")
updatedDoc = updatedDoc.copy( updatedDoc = updatedDoc.copy(
filePath = file.absolutePath, filePath = file.absolutePath,
lastModified = file.lastModified() lastModified = file.lastModified()
@ -128,7 +158,7 @@ class PdfScanner(
// 是否加密更新 // 是否加密更新
val currentIsPassword = isPdfEncrypted(file) val currentIsPassword = isPdfEncrypted(file)
if (existingDoc.isPassword != currentIsPassword) { if (existingDoc.isPassword != currentIsPassword) {
LogUtil.logDebug(TAG, "PdfScanner: ✅ 密码状态需要更新") LogUtil.logDebug(TAG, "✅ 密码状态需要更新")
updatedDoc = updatedDoc.copy(isPassword = currentIsPassword) updatedDoc = updatedDoc.copy(isPassword = currentIsPassword)
needUpdate = true needUpdate = true
} }
@ -137,8 +167,9 @@ class PdfScanner(
// 如果不是加密 PDF再生成缩略图 // 如果不是加密 PDF再生成缩略图
val newThumbnail = generateThumbnail(context, file) val newThumbnail = generateThumbnail(context, file)
if (existingDoc.thumbnailPath != newThumbnail) { if (existingDoc.thumbnailPath != newThumbnail) {
LogUtil.logDebug(TAG, "PdfScanner: ✅ 缩略图需要更新") LogUtil.logDebug(TAG, "✅ 缩略图需要更新")
updatedDoc = updatedDoc.copy(thumbnailPath = newThumbnail) updatedDoc =
updatedDoc.copy(thumbnailPath = newThumbnail)
needUpdate = true needUpdate = true
} }
} else { } else {
@ -150,40 +181,51 @@ class PdfScanner(
if (needUpdate) { if (needUpdate) {
pdfRepository.insertOrUpdateDocument(updatedDoc) pdfRepository.insertOrUpdateDocument(updatedDoc)
LogUtil.logDebug( LogUtil.logDebug(
TAG, "PdfScanner: ✅ 数据库已更新: ${file.name}" TAG, "✅ 数据库已更新: ${file.name}"
) )
} else { } else {
LogUtil.logDebug( LogUtil.logDebug(
TAG, "PdfScanner: ⏩ 无需更新: ${file.name}" TAG, "⏩ 无需更新: ${file.name}"
) )
} }
} }
} }
} }
} }
// 打印数据库中的总记录数 // 打印数据库中的总记录数
pdfRepository.getAllDocumentsOnce().forEach { doc -> pdfRepository.getAllDocumentsOnce().forEach { doc ->
LogUtil.logDebug( LogUtil.logDebug(
TAG, "PdfScanner: 📖 ${doc.fileName} - ${doc.pageCount}页 - ${ TAG, " 📖 ${doc.fileName} - ${doc.pageCount}页 - ${
FileUtils.formatFileSize( FileUtils.formatFileSize(
doc.fileSize doc.fileSize
) )
} - ${doc.thumbnailPath}" } - ${doc.thumbnailPath}"
) )
} }
}
// 标记扫描完成 // 标记扫描完成
ScanManager.markScanComplete(context) ScanManager.markScanComplete(context)
val lastScanTime = ScanManager.getLastScanTime(context) val lastScanTime = ScanManager.getLastScanTime(context)
LogUtil.logDebug( LogUtil.logDebug(
TAG, "PdfScanner: ✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}" TAG, "✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}"
) )
var string = if (needFullScan) {
"全盘扫描"
} else if (cachedDocs.isEmpty()) {
"数据库没有值,第一次全盘扫描"
} else {
"快速扫描"
}
// 计算扫描耗时
val scannerTime = System.currentTimeMillis() - startScannerTime
LogUtil.logDebug(
TAG,
"$string 本次扫描耗时: $scannerTime ms (${scannerTime / 1000.0} 秒)"
)
PDFReaderApplication.isNeedFullScan = false
callback.invoke(true) callback.invoke(true)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "PdfScanner: ❌ 扫描出错: ${e.message}", e) Log.e(TAG, "❌ 扫描出错: ${e.message}", e)
callback.invoke(false) callback.invoke(false)
} }
} }