update
This commit is contained in:
parent
daaa2e4de5
commit
810399bcf8
@ -3,24 +3,31 @@ package com.all.pdfreader.pro.app
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.all.pdfreader.pro.app.room.repository.PdfRepository
|
||||
import com.all.pdfreader.pro.app.util.FileChangeObserver
|
||||
|
||||
class PDFReaderApplication : Application() {
|
||||
|
||||
companion object {
|
||||
private lateinit var instance: PDFReaderApplication
|
||||
|
||||
fun getInstance(): PDFReaderApplication = instance
|
||||
|
||||
fun getContext(): Context = instance.applicationContext
|
||||
}
|
||||
|
||||
private lateinit var fileChangeObserver: FileChangeObserver
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
|
||||
// 初始化文件变化监听(不立即启动)
|
||||
fileChangeObserver = FileChangeObserver(this)
|
||||
|
||||
// 初始化数据库
|
||||
PdfRepository.initialize(this)
|
||||
}
|
||||
|
||||
|
||||
// 在权限授权后调用
|
||||
fun startFileChangeObserving() {
|
||||
fileChangeObserver.startObserving()
|
||||
}
|
||||
}
|
||||
@ -28,6 +28,9 @@ interface PdfDocumentDao {
|
||||
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
|
||||
fun getAllDocuments(): Flow<List<PdfDocumentEntity>>
|
||||
|
||||
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
|
||||
suspend fun getAllDocumentsOnce(): List<PdfDocumentEntity>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(document: PdfDocumentEntity)
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package com.all.pdfreader.pro.app.room.entity
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordDialog
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@ -30,5 +31,8 @@ data class PdfDocumentEntity(
|
||||
val metadataSubject: String? = null, // PDF元数据主题
|
||||
val metadataKeywords: String? = null, // PDF元数据关键词
|
||||
val metadataCreationDate: Long? = null, // PDF创建时间
|
||||
val metadataModificationDate: Long? = null // PDF修改时间
|
||||
): Parcelable
|
||||
val metadataModificationDate: Long? = null, // PDF修改时间
|
||||
|
||||
val password: String? = null,// PDF密码(加密存储)
|
||||
val isPassword: Boolean = false//是否存在密码
|
||||
) : Parcelable
|
||||
@ -34,9 +34,12 @@ class PdfRepository private constructor(context: Context) {
|
||||
return pdfDao.getByPath(filePath)
|
||||
}
|
||||
|
||||
suspend fun getAllDocumentsOnce(): List<PdfDocumentEntity> = pdfDao.getAllDocumentsOnce()
|
||||
|
||||
fun getAllDocuments(): Flow<List<PdfDocumentEntity>> = pdfDao.getAllDocuments()
|
||||
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>> = pdfDao.getFavoriteDocuments()
|
||||
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>> = pdfDao.searchDocuments(query)
|
||||
fun searchDocuments(query: String): Flow<List<PdfDocumentEntity>> =
|
||||
pdfDao.searchDocuments(query)
|
||||
|
||||
suspend fun updateFavoriteStatus(fileHash: String, isFavorite: Boolean) {
|
||||
val document = pdfDao.getByHash(fileHash)?.copy(
|
||||
@ -55,13 +58,32 @@ class PdfRepository private constructor(context: Context) {
|
||||
document?.let { pdfDao.update(it) }
|
||||
}
|
||||
|
||||
suspend fun updatePasswordStatus(fileHash: String, isPassword: Boolean) {
|
||||
val document = pdfDao.getByHash(fileHash)?.copy(
|
||||
isPassword = isPassword
|
||||
)
|
||||
document?.let { pdfDao.update(it) }
|
||||
}
|
||||
|
||||
suspend fun updatePassword(fileHash: String, password: String?) {
|
||||
val document = pdfDao.getByHash(fileHash)?.copy(
|
||||
password = password
|
||||
)
|
||||
document?.let { pdfDao.update(it) }
|
||||
}
|
||||
|
||||
// 最近阅读相关操作
|
||||
suspend fun addToRecent(pdfHash: String, page: Int = 0) {
|
||||
val existing = recentDao.getByPdfHash(pdfHash)
|
||||
if (existing != null) {
|
||||
recentDao.updateOpenTime(pdfHash)
|
||||
} else {
|
||||
recentDao.insertOrUpdate(RecentReadEntity(pdfHash = pdfHash, lastOpenedTime = System.currentTimeMillis()))
|
||||
recentDao.insertOrUpdate(
|
||||
RecentReadEntity(
|
||||
pdfHash = pdfHash,
|
||||
lastOpenedTime = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,21 +93,28 @@ class PdfRepository private constructor(context: Context) {
|
||||
suspend fun addBookmark(bookmark: BookmarkEntity): Long = bookmarkDao.insert(bookmark)
|
||||
suspend fun updateBookmark(bookmark: BookmarkEntity) = bookmarkDao.update(bookmark)
|
||||
suspend fun deleteBookmark(bookmark: BookmarkEntity) = bookmarkDao.delete(bookmark)
|
||||
fun getBookmarksByPdf(pdfHash: String): Flow<List<BookmarkEntity>> = bookmarkDao.getBookmarksByPdf(pdfHash)
|
||||
suspend fun getBookmarksByPage(pdfHash: String, page: Int): List<BookmarkEntity> = bookmarkDao.getBookmarksByPage(pdfHash, page)
|
||||
fun getBookmarksByPdf(pdfHash: String): Flow<List<BookmarkEntity>> =
|
||||
bookmarkDao.getBookmarksByPdf(pdfHash)
|
||||
|
||||
suspend fun getBookmarksByPage(pdfHash: String, page: Int): List<BookmarkEntity> =
|
||||
bookmarkDao.getBookmarksByPage(pdfHash, page)
|
||||
|
||||
// 注释相关操作
|
||||
suspend fun addNote(note: NoteEntity): Long = noteDao.insert(note)
|
||||
suspend fun updateNote(note: NoteEntity) = noteDao.update(note)
|
||||
suspend fun deleteNote(note: NoteEntity) = noteDao.delete(note)
|
||||
fun getNotesByPdf(pdfHash: String): Flow<List<NoteEntity>> = noteDao.getNotesByPdf(pdfHash)
|
||||
suspend fun getNotesByPage(pdfHash: String, page: Int): List<NoteEntity> = noteDao.getNotesByPage(pdfHash, page)
|
||||
fun getNotesByType(pdfHash: String, noteType: String): Flow<List<NoteEntity>> = noteDao.getNotesByType(pdfHash, noteType)
|
||||
suspend fun getNotesByPage(pdfHash: String, page: Int): List<NoteEntity> =
|
||||
noteDao.getNotesByPage(pdfHash, page)
|
||||
|
||||
fun getNotesByType(pdfHash: String, noteType: String): Flow<List<NoteEntity>> =
|
||||
noteDao.getNotesByType(pdfHash, noteType)
|
||||
|
||||
// 组合查询
|
||||
suspend fun getPdfWithDetails(pdfHash: String): Flow<PdfDetails> {
|
||||
return combine(
|
||||
pdfDao.getByHash(pdfHash)?.let { kotlinx.coroutines.flow.flowOf(it) } ?: kotlinx.coroutines.flow.flowOf(null),
|
||||
pdfDao.getByHash(pdfHash)?.let { kotlinx.coroutines.flow.flowOf(it) }
|
||||
?: kotlinx.coroutines.flow.flowOf(null),
|
||||
bookmarkDao.getBookmarksByPdf(pdfHash),
|
||||
noteDao.getNotesByPdf(pdfHash)
|
||||
) { document, bookmarks, notes ->
|
||||
@ -137,7 +166,8 @@ class PdfRepository private constructor(context: Context) {
|
||||
}
|
||||
|
||||
fun getInstance(): PdfRepository {
|
||||
return INSTANCE ?: throw IllegalStateException("PdfRepository must be initialized first")
|
||||
return INSTANCE
|
||||
?: throw IllegalStateException("PdfRepository must be initialized first")
|
||||
}
|
||||
|
||||
// 向后兼容的方法
|
||||
|
||||
@ -6,6 +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.R
|
||||
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
|
||||
import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment
|
||||
@ -31,7 +32,6 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val pdfRepository = getRepository()
|
||||
private lateinit var fileChangeObserver: FileChangeObserver
|
||||
private lateinit var pdfScanner: PdfScanner
|
||||
|
||||
private val homeFragment = HomeFrag()
|
||||
@ -64,23 +64,6 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
||||
updateSelectedNav(activeFragment)
|
||||
}
|
||||
|
||||
private fun scanningStrategy() {
|
||||
// 智能扫描策略
|
||||
if (pdfScanner.shouldScan()) {
|
||||
logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)")
|
||||
if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
|
||||
lifecycleScope.launch {
|
||||
pdfScanner.scanAndLoadPdfFiles()
|
||||
}
|
||||
} else {
|
||||
logDebug("❌ 权限不足,跳过扫描")
|
||||
}
|
||||
} else {
|
||||
val hoursAgo = pdfScanner.getHoursSinceLastScan()
|
||||
logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFragments() {
|
||||
supportFragmentManager.beginTransaction().add(R.id.fragment_fl, toolsFragment, "TOOLS")
|
||||
.hide(toolsFragment).add(R.id.fragment_fl, favoriteFragment, "FAVORITE")
|
||||
@ -161,7 +144,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
||||
logDebug("main onResume")
|
||||
if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
|
||||
// 有授权才初始化文件变化监听器
|
||||
fileChangeObserver = FileChangeObserver(this, lifecycle)
|
||||
PDFReaderApplication.getInstance().startFileChangeObserving()
|
||||
scanningStrategy()
|
||||
binding.pnLayout.visibility = View.GONE
|
||||
} else {
|
||||
@ -174,12 +157,29 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanningStrategy() {
|
||||
// 智能扫描策略
|
||||
if (pdfScanner.shouldScan()) {
|
||||
logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)")
|
||||
if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
|
||||
lifecycleScope.launch {
|
||||
pdfScanner.scanAndLoadPdfFiles()
|
||||
}
|
||||
} else {
|
||||
logDebug("❌ 权限不足,跳过扫描")
|
||||
}
|
||||
} else {
|
||||
val hoursAgo = pdfScanner.getHoursSinceLastScan()
|
||||
logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前")
|
||||
}
|
||||
}
|
||||
|
||||
// 授权后续操作
|
||||
override fun onPermissionGranted() {
|
||||
logDebug("main onPermissionGranted")
|
||||
//授权成功后:隐藏授权提示,开始扫描文件
|
||||
binding.pnLayout.visibility = View.GONE
|
||||
fileChangeObserver = FileChangeObserver(this, lifecycle)
|
||||
PDFReaderApplication.getInstance().startFileChangeObserving()
|
||||
scanningStrategy()
|
||||
}
|
||||
|
||||
|
||||
@ -2,12 +2,14 @@ package com.all.pdfreader.pro.app.ui.act
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.all.pdfreader.pro.app.R
|
||||
import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding
|
||||
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
||||
import com.all.pdfreader.pro.app.ui.view.CustomScrollHandle
|
||||
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
|
||||
import com.github.barteksc.pdfviewer.listener.OnErrorListener
|
||||
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener
|
||||
import com.github.barteksc.pdfviewer.listener.OnPageChangeListener
|
||||
@ -18,37 +20,82 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
OnErrorListener {
|
||||
override val TAG: String = "PdfViewActivity"
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_PDF_HASH = "extra_pdf_hash"
|
||||
|
||||
// 创建启动Intent的便捷方法
|
||||
fun createIntent(context: Context, filePath: String): Intent {
|
||||
return Intent(context, PdfViewActivity::class.java).apply {
|
||||
putExtra(EXTRA_PDF_HASH, filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityPdfViewBinding
|
||||
private lateinit var pdfDocument: PdfDocumentEntity
|
||||
private val pdfRepository = getRepository()
|
||||
|
||||
private val viewModel by lazy { ViewModelProvider(this)[PdfViewModel::class.java] }
|
||||
private val repository = getRepository()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPdfViewBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// 获取传递的PDF文档数据
|
||||
pdfDocument = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(EXTRA_PDF_DOCUMENT, PdfDocumentEntity::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") intent.getParcelableExtra<PdfDocumentEntity>(EXTRA_PDF_DOCUMENT)
|
||||
} ?: throw IllegalArgumentException("PDF document data is required")
|
||||
val filePath = intent.getStringExtra(EXTRA_PDF_HASH)
|
||||
?: throw IllegalArgumentException("PDF file hash is required")
|
||||
|
||||
loadPdf()
|
||||
// 观察PDF文档数据
|
||||
viewModel.pdfDocument.observe(this) { document ->
|
||||
document?.let {
|
||||
pdfDocument = it
|
||||
loadPdf()
|
||||
} ?: run {
|
||||
showToast(getString(R.string.file_not))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载PDF数据
|
||||
viewModel.getPDFDocument(filePath)
|
||||
}
|
||||
|
||||
private fun loadPdf() {
|
||||
logDebug("loadPdf ->${pdfDocument.lastReadPage} ${pdfDocument.readingProgress}")
|
||||
// 使用传递的文件路径加载PDF
|
||||
val file = File(pdfDocument.filePath)
|
||||
if (file.exists()) {
|
||||
binding.pdfview
|
||||
.fromFile(file)
|
||||
val pdfView = binding.pdfview
|
||||
|
||||
// 如果有密码,先尝试使用密码加载
|
||||
if (!pdfDocument.password.isNullOrEmpty()) {
|
||||
try {
|
||||
pdfView.fromFile(file)
|
||||
.password(pdfDocument.password) // 使用密码加载
|
||||
.defaultPage(pdfDocument.lastReadPage)
|
||||
.enableDoubletap(true)
|
||||
.onLoad(this)
|
||||
.enableAnnotationRendering(true)
|
||||
.onError(this)
|
||||
.onPageChange(this)
|
||||
.scrollHandle(CustomScrollHandle(this))
|
||||
.load()
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
logDebug("Password protected PDF failed: ${e.message}")
|
||||
// 密码错误,显示密码输入对话框
|
||||
showPasswordDialog(file)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 无密码PDF正常加载
|
||||
pdfView.fromFile(file)
|
||||
.defaultPage(pdfDocument.lastReadPage) // 从上次阅读页码开始
|
||||
.enableDoubletap(true)// 是否允许双击缩放
|
||||
.onLoad(this)
|
||||
.enableAnnotationRendering(true) // 是否渲染注释(如评论、颜色、表单等)
|
||||
.onError(this)
|
||||
.onPageChange(this)
|
||||
.scrollHandle(CustomScrollHandle(this))
|
||||
.load()
|
||||
} else {
|
||||
showToast(getString(R.string.file_not) + ": ${pdfDocument.fileName}")
|
||||
@ -63,38 +110,103 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
|
||||
|
||||
//PDF 加载出错时回调
|
||||
override fun onError(t: Throwable?) {
|
||||
logDebug("PDF loading error: ${t?.message}")
|
||||
t?.let {
|
||||
val errorMessage = it.message ?: "未知错误"
|
||||
|
||||
// 检查是否是密码相关的错误
|
||||
if (errorMessage.contains("Password") || errorMessage.contains("password")) {
|
||||
// 如果当前没有设置密码,显示密码输入对话框
|
||||
if (pdfDocument.password.isNullOrEmpty()) {
|
||||
val file = File(pdfDocument.filePath)
|
||||
showPasswordDialog(file)
|
||||
} else {
|
||||
// 密码错误,显示密码输入对话框
|
||||
showToast("PDF密码错误")
|
||||
val file = File(pdfDocument.filePath)
|
||||
showPasswordDialog(file)
|
||||
}
|
||||
} else {
|
||||
// 其他错误
|
||||
showToast("PDF加载失败: $errorMessage")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//页面切换时回调
|
||||
override fun onPageChanged(page: Int, pageCount: Int) {
|
||||
// 保存阅读进度
|
||||
// 保存阅读进度到内存
|
||||
pdfDocument = pdfDocument.copy(
|
||||
lastReadPage = page, readingProgress = (page.toFloat() / pageCount.toFloat()) * 100
|
||||
)
|
||||
saveReadingProgress()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
saveReadingProgress()
|
||||
}
|
||||
|
||||
private fun saveReadingProgress() {
|
||||
lifecycleScope.launch {
|
||||
pdfRepository.updateReadingProgress(
|
||||
repository.updateReadingProgress(
|
||||
pdfDocument.fileHash, pdfDocument.lastReadPage, pdfDocument.readingProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_PDF_DOCUMENT = "extra_pdf_document"
|
||||
private fun showPasswordDialog(file: File) {
|
||||
val builder = android.app.AlertDialog.Builder(this)
|
||||
builder.setTitle("PDF密码保护")
|
||||
builder.setMessage("请输入PDF文件密码:")
|
||||
|
||||
// 创建启动Intent的便捷方法
|
||||
fun createIntent(context: Context, pdfDocument: PdfDocumentEntity): Intent {
|
||||
return Intent(context, PdfViewActivity::class.java).apply {
|
||||
putExtra(EXTRA_PDF_DOCUMENT, pdfDocument)
|
||||
val input = android.widget.EditText(this)
|
||||
input.inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
builder.setView(input)
|
||||
|
||||
builder.setPositiveButton("确定") { dialog, _ ->
|
||||
val password = input.text.toString()
|
||||
if (password.isNotEmpty()) {
|
||||
// 尝试使用输入的密码重新加载PDF
|
||||
tryLoadPdfWithPassword(file, password)
|
||||
} else {
|
||||
showToast("密码不能为空")
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
builder.setNegativeButton("取消") { dialog, _ ->
|
||||
dialog.cancel()
|
||||
finish()
|
||||
}
|
||||
|
||||
builder.setOnCancelListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun tryLoadPdfWithPassword(file: File, password: String) {
|
||||
try {
|
||||
binding.pdfview.fromFile(file)
|
||||
.password(password) // 使用输入的密码
|
||||
.defaultPage(pdfDocument.lastReadPage)
|
||||
.enableDoubletap(true)
|
||||
.onLoad(this)
|
||||
.enableAnnotationRendering(true)
|
||||
.onError { error ->
|
||||
logDebug("Password still incorrect: ${error?.message}")
|
||||
showToast("密码错误,请重试")
|
||||
showPasswordDialog(file) // 重新显示密码对话框
|
||||
}
|
||||
.onPageChange(this)
|
||||
.scrollHandle(CustomScrollHandle(this))
|
||||
.load()
|
||||
} catch (e: Exception) {
|
||||
logDebug("Failed to load PDF with provided password: ${e.message}")
|
||||
showToast("密码错误,请重试")
|
||||
showPasswordDialog(file) // 重新显示密码对话框
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.all.pdfreader.pro.app.ui.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.all.pdfreader.pro.app.databinding.DialogPdfPasswordBinding
|
||||
|
||||
class PdfPasswordDialog : DialogFragment() {
|
||||
|
||||
private var _binding: DialogPdfPasswordBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var listener: PasswordDialogListener? = null
|
||||
|
||||
interface PasswordDialogListener {
|
||||
fun onPasswordSet(password: String?)
|
||||
fun onPasswordDialogCancelled()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(): PdfPasswordDialog {
|
||||
return PdfPasswordDialog()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
_binding = DialogPdfPasswordBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupListeners()
|
||||
setupTextWatchers()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.tvCancel.setOnClickListener {
|
||||
listener?.onPasswordDialogCancelled()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.tvConfirm.setOnClickListener {
|
||||
val password = binding.etPassword.text.toString()
|
||||
val confirmPassword = binding.etConfirmPassword.text.toString()
|
||||
|
||||
if (validatePassword(password, confirmPassword)) {
|
||||
listener?.onPasswordSet(password)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTextWatchers() {
|
||||
binding.etPassword.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) {}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
validateInput()
|
||||
}
|
||||
})
|
||||
|
||||
binding.etConfirmPassword.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) {}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
validateInput()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun validateInput() {
|
||||
val password = binding.etPassword.text.toString()
|
||||
val confirmPassword = binding.etConfirmPassword.text.toString()
|
||||
|
||||
binding.tilPassword.error = null
|
||||
binding.tilConfirmPassword.error = null
|
||||
|
||||
if (password.isNotEmpty() && confirmPassword.isNotEmpty() && password != confirmPassword) {
|
||||
binding.tilConfirmPassword.error = "密码不匹配"
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatePassword(password: String, confirmPassword: String): Boolean {
|
||||
binding.tilPassword.error = null
|
||||
binding.tilConfirmPassword.error = null
|
||||
|
||||
// 允许空密码(表示不设置密码)
|
||||
if (password.isEmpty()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 密码长度验证
|
||||
if (password.length < 4) {
|
||||
binding.tilPassword.error = "密码长度至少为4位"
|
||||
return false
|
||||
}
|
||||
|
||||
// 密码匹配验证
|
||||
if (password != confirmPassword) {
|
||||
binding.tilConfirmPassword.error = "密码不匹配"
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context is PasswordDialogListener) {
|
||||
listener = context
|
||||
} else {
|
||||
throw RuntimeException("$context must implement PasswordDialogListener")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
listener = null
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,7 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
|
||||
|
||||
private fun initView() {
|
||||
adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf ->
|
||||
val intent = PdfViewActivity.createIntent(requireContext(), pdf)
|
||||
val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
|
||||
startActivity(intent)
|
||||
|
||||
}, onMoreClick = { pdf ->
|
||||
|
||||
@ -0,0 +1,247 @@
|
||||
package com.all.pdfreader.pro.app.ui.view;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.all.pdfreader.pro.app.R;
|
||||
import com.github.barteksc.pdfviewer.PDFView;
|
||||
import com.github.barteksc.pdfviewer.scroll.ScrollHandle;
|
||||
import com.github.barteksc.pdfviewer.util.Util;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class CustomScrollHandle extends RelativeLayout implements ScrollHandle {
|
||||
private final static int HANDLE_LONG = 36;
|
||||
private final static int HANDLE_SHORT = 28;
|
||||
private final static int DEFAULT_TEXT_SIZE = 12;
|
||||
|
||||
private float relativeHandlerMiddle = 0f;
|
||||
|
||||
protected TextView textView;
|
||||
protected Context context;
|
||||
private final boolean inverted;
|
||||
private PDFView pdfView;
|
||||
private float currentPos;
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
private final Runnable hidePageScrollerRunnable = this::hide;
|
||||
|
||||
public CustomScrollHandle(Context context) {
|
||||
this(context, false);
|
||||
}
|
||||
|
||||
public CustomScrollHandle(Context context, boolean inverted) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.inverted = inverted;
|
||||
textView = new TextView(context);
|
||||
setVisibility(INVISIBLE);
|
||||
setTextColor(Color.BLACK);
|
||||
setTextSize(DEFAULT_TEXT_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupLayout(PDFView pdfView) {
|
||||
int align, width, height;
|
||||
Drawable background;
|
||||
// determine handler position, default is right (when scrolling vertically) or bottom (when scrolling horizontally)
|
||||
if (pdfView.isSwipeVertical()) {
|
||||
width = HANDLE_LONG;
|
||||
height = HANDLE_SHORT;
|
||||
if (inverted) { // left
|
||||
align = ALIGN_PARENT_LEFT;
|
||||
background = ContextCompat.getDrawable(context, R.drawable.dr_scroll_handle_left);
|
||||
} else { // right
|
||||
align = ALIGN_PARENT_RIGHT;
|
||||
background = ContextCompat.getDrawable(context, R.drawable.dr_scroll_handle_right);
|
||||
}
|
||||
} else {
|
||||
width = HANDLE_SHORT;
|
||||
height = HANDLE_LONG;
|
||||
if (inverted) { // top
|
||||
align = ALIGN_PARENT_TOP;
|
||||
background = ContextCompat.getDrawable(context, R.drawable.dr_scroll_handle_top);
|
||||
} else { // bottom
|
||||
align = ALIGN_PARENT_BOTTOM;
|
||||
background = ContextCompat.getDrawable(context, R.drawable.dr_scroll_handle_bottom);
|
||||
}
|
||||
}
|
||||
|
||||
setBackground(background);
|
||||
|
||||
LayoutParams lp = new LayoutParams(Util.getDP(context, width), Util.getDP(context, height));
|
||||
lp.setMargins(0, 0, 0, 0);
|
||||
|
||||
LayoutParams tvlp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
tvlp.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
|
||||
|
||||
addView(textView, tvlp);
|
||||
|
||||
lp.addRule(align);
|
||||
pdfView.addView(this, lp);
|
||||
|
||||
this.pdfView = pdfView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyLayout() {
|
||||
pdfView.removeView(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScroll(float position) {
|
||||
if (!shown()) {
|
||||
show();
|
||||
} else {
|
||||
handler.removeCallbacks(hidePageScrollerRunnable);
|
||||
}
|
||||
if (pdfView != null) {
|
||||
setPosition((pdfView.isSwipeVertical() ? pdfView.getHeight() : pdfView.getWidth()) * position);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPosition(float pos) {
|
||||
if (Float.isInfinite(pos) || Float.isNaN(pos)) {
|
||||
return;
|
||||
}
|
||||
float pdfViewSize;
|
||||
if (pdfView.isSwipeVertical()) {
|
||||
pdfViewSize = pdfView.getHeight();
|
||||
} else {
|
||||
pdfViewSize = pdfView.getWidth();
|
||||
}
|
||||
pos -= relativeHandlerMiddle;
|
||||
|
||||
if (pos < 0) {
|
||||
pos = 0;
|
||||
} else if (pos > pdfViewSize - Util.getDP(context, HANDLE_SHORT)) {
|
||||
pos = pdfViewSize - Util.getDP(context, HANDLE_SHORT);
|
||||
}
|
||||
|
||||
if (pdfView.isSwipeVertical()) {
|
||||
setY(pos);
|
||||
} else {
|
||||
setX(pos);
|
||||
}
|
||||
|
||||
calculateMiddle();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void calculateMiddle() {
|
||||
float pos, viewSize, pdfViewSize;
|
||||
if (pdfView.isSwipeVertical()) {
|
||||
pos = getY();
|
||||
viewSize = getHeight();
|
||||
pdfViewSize = pdfView.getHeight();
|
||||
} else {
|
||||
pos = getX();
|
||||
viewSize = getWidth();
|
||||
pdfViewSize = pdfView.getWidth();
|
||||
}
|
||||
relativeHandlerMiddle = ((pos + relativeHandlerMiddle) / pdfViewSize) * viewSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideDelayed() {
|
||||
handler.postDelayed(hidePageScrollerRunnable, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPageNum(int pageNum) {
|
||||
String text = String.valueOf(pageNum);
|
||||
if (!textView.getText().equals(text)) {
|
||||
textView.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shown() {
|
||||
return getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
if (getVisibility() != VISIBLE) {
|
||||
setTranslationX(getWidth()); // 初始在右侧外面
|
||||
setAlpha(0f);
|
||||
setVisibility(VISIBLE);
|
||||
animate().translationX(0)
|
||||
.alpha(1f)
|
||||
.setDuration(450)
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
if (getVisibility() == VISIBLE) {
|
||||
animate().translationX(getWidth()) // 滑到右边外面
|
||||
.alpha(0f)
|
||||
.setDuration(450)
|
||||
.withEndAction(() -> setVisibility(INVISIBLE))
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTextColor(int color) {
|
||||
textView.setTextColor(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param size text size in dp
|
||||
*/
|
||||
public void setTextSize(int size) {
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, size);
|
||||
}
|
||||
|
||||
private boolean isPDFViewReady() {
|
||||
return pdfView != null && pdfView.getPageCount() > 0 && !pdfView.documentFitsView();
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
|
||||
if (!isPDFViewReady()) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
pdfView.stopFling();
|
||||
handler.removeCallbacks(hidePageScrollerRunnable);
|
||||
if (pdfView.isSwipeVertical()) {
|
||||
currentPos = event.getRawY() - getY();
|
||||
} else {
|
||||
currentPos = event.getRawX() - getX();
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (pdfView.isSwipeVertical()) {
|
||||
setPosition(event.getRawY() - currentPos + relativeHandlerMiddle);
|
||||
pdfView.setPositionOffset(relativeHandlerMiddle / (float) getHeight(), false);
|
||||
} else {
|
||||
setPosition(event.getRawX() - currentPos + relativeHandlerMiddle);
|
||||
pdfView.setPositionOffset(relativeHandlerMiddle / (float) getWidth(), false);
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
hideDelayed();
|
||||
pdfView.performPageSnap();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.all.pdfreader.pro.app.util
|
||||
|
||||
import android.content.Context
|
||||
@ -9,20 +7,15 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.collections.distinctBy
|
||||
import kotlin.collections.plus
|
||||
|
||||
class FileChangeObserver(
|
||||
private val context: Context,
|
||||
private val lifecycle: Lifecycle
|
||||
) : LifecycleObserver {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
@ -33,11 +26,6 @@ class FileChangeObserver(
|
||||
private var lastScanTime = 0L
|
||||
private val debounceDelay = 3000L // 3秒防抖
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun startObserving() {
|
||||
if (isObserving) return
|
||||
|
||||
@ -61,7 +49,6 @@ class FileChangeObserver(
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
fun stopObserving() {
|
||||
if (!isObserving) return
|
||||
|
||||
|
||||
@ -2,11 +2,14 @@ package com.all.pdfreader.pro.app.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
@ -313,4 +316,32 @@ object FileUtils {
|
||||
data class FileInfo(
|
||||
val name: String, val size: Long, val uri: Uri
|
||||
)
|
||||
|
||||
/**
|
||||
* 判断 PDF 是否加密(基于原生 PdfRenderer)
|
||||
*
|
||||
* @param file 要检测的 PDF 文件
|
||||
* @return true = 已加密 / 不能打开,false = 未加密
|
||||
*/
|
||||
fun isPdfEncrypted(file: File): Boolean {
|
||||
var fd: ParcelFileDescriptor? = null
|
||||
var renderer: PdfRenderer? = null
|
||||
return try {
|
||||
fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
renderer = PdfRenderer(fd)
|
||||
// 能成功打开 => 没有加密
|
||||
false
|
||||
} catch (e: SecurityException) {
|
||||
// PdfRenderer 在遇到加密 PDF 时可能抛 SecurityException
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
// 某些加密 PDF 也可能走 IOException
|
||||
e.message?.contains("password", ignoreCase = true) == true
|
||||
} finally {
|
||||
try {
|
||||
renderer?.close()
|
||||
fd?.close()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,42 +7,41 @@ import android.graphics.Color
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.createBitmap
|
||||
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
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import androidx.core.graphics.createBitmap
|
||||
import com.github.barteksc.pdfviewer.PDFView
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class PdfScanner(
|
||||
private val context: Context,
|
||||
private val pdfRepository: PdfRepository
|
||||
private val context: Context, private val pdfRepository: PdfRepository
|
||||
) {
|
||||
|
||||
private val TAG = "ocean-PdfScanner"
|
||||
|
||||
suspend fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) {
|
||||
if (!StoragePermissionHelper.hasBasicStoragePermission(context)) {
|
||||
LogUtil.logDebug("ocean", "PdfScanner: 权限不足")
|
||||
LogUtil.logDebug(TAG, "PdfScanner: 权限不足")
|
||||
callback.invoke(false)
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
LogUtil.logDebug("ocean", "PdfScanner: 🔍 开始扫描PDF文件...")
|
||||
LogUtil.logDebug(TAG, "PdfScanner: 🔍 开始扫描PDF文件...")
|
||||
|
||||
// 扫描应用私有目录(无需权限)
|
||||
val privateFiles = FileUtils.scanPdfFiles(context)
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件"
|
||||
TAG, "PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件"
|
||||
)
|
||||
privateFiles.forEach { file ->
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
TAG,
|
||||
"PdfScanner: 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})"
|
||||
)
|
||||
}
|
||||
@ -50,41 +49,44 @@ class PdfScanner(
|
||||
// 扫描MediaStore(需要权限)
|
||||
val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(context)
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件"
|
||||
TAG, "PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件"
|
||||
)
|
||||
mediaStoreFiles.forEach { file ->
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
TAG,
|
||||
"PdfScanner: 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})"
|
||||
)
|
||||
}
|
||||
|
||||
// 合并并去重
|
||||
val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath }
|
||||
LogUtil.logDebug("ocean", "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件")
|
||||
LogUtil.logDebug(TAG, "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件")
|
||||
|
||||
// 处理每个PDF文件
|
||||
allFiles.forEachIndexed { index, file ->
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: 🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name}"
|
||||
TAG,
|
||||
"PdfScanner: 🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name} - ${file.absolutePath}"
|
||||
)
|
||||
|
||||
if (FileUtils.isPdfFile(file)) {
|
||||
val fileHash = FileUtils.calculateFileHash(file.absolutePath)
|
||||
LogUtil.logDebug("ocean", "PdfScanner: 🔑 文件哈希: $fileHash")
|
||||
LogUtil.logDebug(TAG, "PdfScanner: 🔑 文件哈希: $fileHash")
|
||||
|
||||
if (fileHash != null) {
|
||||
val existingDoc = pdfRepository.getDocumentByHash(fileHash)
|
||||
val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath)
|
||||
|
||||
if (existingDoc == null) {
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: 🆕 发现新PDF文件: ${file.name}"
|
||||
TAG, "PdfScanner: 🆕 发现新PDF文件: ${file.name}"
|
||||
)
|
||||
val thumbnailPath = generateThumbnail(context, file)
|
||||
|
||||
var thumbnailPath: String? = null
|
||||
val isPassword = isPdfEncrypted(file)
|
||||
LogUtil.logDebug(TAG, "PdfScanner: isPassword->${isPassword}")
|
||||
if (!isPassword) {//没有密码的情况下才去获取缩略图
|
||||
thumbnailPath = generateThumbnail(context, file) ?: ""
|
||||
}
|
||||
LogUtil.logDebug(TAG, "PdfScanner: thumbnailPath->${thumbnailPath}")
|
||||
val metadata =
|
||||
PdfMetadataExtractor.extractMetadata(file.absolutePath)
|
||||
val document = PdfDocumentEntity(
|
||||
@ -100,28 +102,60 @@ class PdfScanner(
|
||||
metadataSubject = metadata?.subject,
|
||||
metadataKeywords = metadata?.keywords,
|
||||
metadataCreationDate = metadata?.creationDate?.time,
|
||||
metadataModificationDate = metadata?.modificationDate?.time
|
||||
metadataModificationDate = metadata?.modificationDate?.time,
|
||||
isPassword = isPassword
|
||||
)
|
||||
pdfRepository.insertOrUpdateDocument(document)
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: ✅ 已保存到数据库: ${file.name}"
|
||||
TAG, "PdfScanner: ✅ 已保存到数据库: ${file.name}"
|
||||
)
|
||||
} else {
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: 📋 文件已存在: ${file.name}"
|
||||
)
|
||||
if (existingDoc.filePath != file.absolutePath) {
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: 🔄 更新文件路径: ${existingDoc.filePath} -> ${file.absolutePath}"
|
||||
)
|
||||
val updatedDoc = existingDoc.copy(
|
||||
LogUtil.logDebug(TAG, "PdfScanner: 📋 文件已存在: ${file.name}")
|
||||
// 🔹 文件已存在,检查是否需要更新
|
||||
var needUpdate = false
|
||||
var updatedDoc = existingDoc.copy()
|
||||
|
||||
// 路径/修改时间更新
|
||||
if (existingDoc.filePath != file.absolutePath || existingDoc.lastModified != file.lastModified()) {
|
||||
LogUtil.logDebug(TAG, "PdfScanner: ✅ 路径/修改时间需要更新")
|
||||
updatedDoc = updatedDoc.copy(
|
||||
filePath = file.absolutePath,
|
||||
lastModified = file.lastModified()
|
||||
)
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
// 是否加密更新
|
||||
val currentIsPassword = isPdfEncrypted(file)
|
||||
if (existingDoc.isPassword != currentIsPassword) {
|
||||
LogUtil.logDebug(TAG, "PdfScanner: ✅ 密码状态需要更新")
|
||||
updatedDoc = updatedDoc.copy(isPassword = currentIsPassword)
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
if (!currentIsPassword) {
|
||||
// 如果不是加密 PDF,再生成缩略图
|
||||
val newThumbnail = generateThumbnail(context, file)
|
||||
if (existingDoc.thumbnailPath != newThumbnail) {
|
||||
LogUtil.logDebug(TAG, "PdfScanner: ✅ 缩略图需要更新")
|
||||
updatedDoc = updatedDoc.copy(thumbnailPath = newThumbnail)
|
||||
needUpdate = true
|
||||
}
|
||||
} else {
|
||||
updatedDoc = updatedDoc.copy(thumbnailPath = null)
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
if (needUpdate) {
|
||||
pdfRepository.insertOrUpdateDocument(updatedDoc)
|
||||
LogUtil.logDebug(
|
||||
TAG, "PdfScanner: ✅ 数据库已更新: ${file.name}"
|
||||
)
|
||||
} else {
|
||||
LogUtil.logDebug(
|
||||
TAG, "PdfScanner: ⏩ 无需更新: ${file.name}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,31 +163,27 @@ class PdfScanner(
|
||||
}
|
||||
|
||||
// 打印数据库中的总记录数
|
||||
pdfRepository.getAllDocuments().collect { docs ->
|
||||
LogUtil.logDebug("ocean", "PdfScanner: 📊 数据库中共有: ${docs.size} 个PDF记录")
|
||||
docs.forEach { doc ->
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: 📖 ${doc.fileName} - ${doc.pageCount}页 - ${
|
||||
FileUtils.formatFileSize(
|
||||
doc.fileSize
|
||||
)
|
||||
} - ${doc.thumbnailPath}"
|
||||
)
|
||||
}
|
||||
|
||||
// 标记扫描完成
|
||||
ScanManager.markScanComplete(context)
|
||||
val lastScanTime = ScanManager.getLastScanTime(context)
|
||||
pdfRepository.getAllDocumentsOnce().forEach { doc ->
|
||||
LogUtil.logDebug(
|
||||
"ocean",
|
||||
"PdfScanner: ✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}"
|
||||
TAG, "PdfScanner: 📖 ${doc.fileName} - ${doc.pageCount}页 - ${
|
||||
FileUtils.formatFileSize(
|
||||
doc.fileSize
|
||||
)
|
||||
} - ${doc.thumbnailPath}"
|
||||
)
|
||||
callback.invoke(true)
|
||||
}
|
||||
|
||||
// 标记扫描完成
|
||||
ScanManager.markScanComplete(context)
|
||||
val lastScanTime = ScanManager.getLastScanTime(context)
|
||||
LogUtil.logDebug(
|
||||
TAG, "PdfScanner: ✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}"
|
||||
)
|
||||
|
||||
callback.invoke(true)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("ocean", "PdfScanner: ❌ 扫描出错: ${e.message}", e)
|
||||
Log.e(TAG, "PdfScanner: ❌ 扫描出错: ${e.message}", e)
|
||||
callback.invoke(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
package com.all.pdfreader.pro.app.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
|
||||
import com.all.pdfreader.pro.app.room.repository.PdfRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PdfViewModel : ViewModel() {
|
||||
private val pdfRepository = PdfRepository.getInstance()
|
||||
|
||||
private val _pdfDocument = MutableLiveData<PdfDocumentEntity?>()
|
||||
val pdfDocument: LiveData<PdfDocumentEntity?> = _pdfDocument
|
||||
|
||||
fun getPDFDocument(filePath: String) {
|
||||
viewModelScope.launch {
|
||||
val document = pdfRepository.getDocumentByPath(filePath)
|
||||
Log.d("ocean", "getPDFDocument->loadPdf: filePath=$filePath, document=$document")
|
||||
_pdfDocument.postValue(document)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable/dr_pdf_scroll_handle.xml
Normal file
9
app/src/main/res/drawable/dr_pdf_scroll_handle.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:bottomLeftRadius="16dp"
|
||||
android:topLeftRadius="16dp" />
|
||||
<solid android:color="@color/grey" />
|
||||
|
||||
</shape>
|
||||
12
app/src/main/res/drawable/dr_scroll_handle_bottom.xml
Normal file
12
app/src/main/res/drawable/dr_scroll_handle_bottom.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:color="#6C7A89"
|
||||
android:width="1dp" />
|
||||
<corners
|
||||
android:topLeftRadius="16dp"
|
||||
android:topRightRadius="16dp" />
|
||||
<solid android:color="#DADFE1" />
|
||||
</shape>
|
||||
6
app/src/main/res/drawable/dr_scroll_handle_left.xml
Normal file
6
app/src/main/res/drawable/dr_scroll_handle_left.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/dr_scroll_handle_right"
|
||||
android:fromDegrees="180"
|
||||
android:toDegrees="180"
|
||||
android:visible="true" />
|
||||
12
app/src/main/res/drawable/dr_scroll_handle_right.xml
Normal file
12
app/src/main/res/drawable/dr_scroll_handle_right.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:color="#6C7A89"
|
||||
android:width="1dp" />
|
||||
<corners
|
||||
android:bottomLeftRadius="16dp"
|
||||
android:topLeftRadius="16dp" />
|
||||
<solid android:color="#DADFE1" />
|
||||
</shape>
|
||||
6
app/src/main/res/drawable/dr_scroll_handle_top.xml
Normal file
6
app/src/main/res/drawable/dr_scroll_handle_top.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:drawable="@drawable/dr_scroll_handle_bottom"
|
||||
android:fromDegrees="180"
|
||||
android:toDegrees="180"
|
||||
android:visible="true" />
|
||||
@ -6,6 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/statusLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:gravity="center_vertical"
|
||||
@ -41,4 +42,36 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/navigationLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/lockBtn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/lockIv"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lockTv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/poppins_regular"
|
||||
android:text="@string/home"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -39,7 +39,7 @@
|
||||
android:id="@+id/tvFileName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:ellipsize="middle"
|
||||
android:maxLines="1"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/black"
|
||||
|
||||
90
app/src/main/res/layout/dialog_pdf_password.xml
Normal file
90
app/src/main/res/layout/dialog_pdf_password.xml
Normal file
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:background="@android:color/white">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="设置PDF密码"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="为PDF文件设置保护密码,留空表示不设置密码"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="输入密码"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:endIconMode="password_toggle"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilConfirmPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="确认密码"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:endIconMode="password_toggle"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etConfirmPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="取消"
|
||||
android:textColor="@color/black"
|
||||
android:padding="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvConfirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="确认"
|
||||
android:textColor="@color/black"
|
||||
android:padding="12dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -24,4 +24,8 @@
|
||||
<string name="permission_denied">Permission Denied</string>
|
||||
<string name="pd_content_notice">Unable to access PDF file, please grant storage permission in settings</string>
|
||||
<string name="file_not">File does not exist</string>
|
||||
<string name="lock">Lock</string>
|
||||
<string name="unlock">Unlock</string>
|
||||
<string name="eye_protect">Eye Protect</string>
|
||||
<string name="bookmarks">Bookmarks</string>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user