This commit is contained in:
ocean 2025-09-05 18:32:32 +08:00
parent daaa2e4de5
commit 810399bcf8
22 changed files with 926 additions and 146 deletions

View File

@ -3,24 +3,31 @@ package com.all.pdfreader.pro.app
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
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.FileChangeObserver
class PDFReaderApplication : Application() { class PDFReaderApplication : Application() {
companion object { companion object {
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
} }
private lateinit var fileChangeObserver: FileChangeObserver
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
// 初始化文件变化监听(不立即启动)
fileChangeObserver = FileChangeObserver(this)
// 初始化数据库 // 初始化数据库
PdfRepository.initialize(this) PdfRepository.initialize(this)
} }
// 在权限授权后调用
fun startFileChangeObserving() {
fileChangeObserver.startObserving()
}
} }

View File

@ -28,6 +28,9 @@ interface PdfDocumentDao {
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC") @Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
fun getAllDocuments(): Flow<List<PdfDocumentEntity>> fun getAllDocuments(): Flow<List<PdfDocumentEntity>>
@Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC")
suspend fun getAllDocumentsOnce(): List<PdfDocumentEntity>
@Delete @Delete
suspend fun delete(document: PdfDocumentEntity) suspend fun delete(document: PdfDocumentEntity)

View File

@ -3,6 +3,7 @@ package com.all.pdfreader.pro.app.room.entity
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordDialog
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@ -30,5 +31,8 @@ data class PdfDocumentEntity(
val metadataSubject: String? = null, // PDF元数据主题 val metadataSubject: String? = null, // PDF元数据主题
val metadataKeywords: String? = null, // PDF元数据关键词 val metadataKeywords: String? = null, // PDF元数据关键词
val metadataCreationDate: Long? = null, // PDF创建时间 val metadataCreationDate: Long? = null, // PDF创建时间
val metadataModificationDate: Long? = null // PDF修改时间 val metadataModificationDate: Long? = null, // PDF修改时间
): Parcelable
val password: String? = null,// PDF密码加密存储
val isPassword: Boolean = false//是否存在密码
) : Parcelable

View File

@ -34,9 +34,12 @@ class PdfRepository private constructor(context: Context) {
return pdfDao.getByPath(filePath) return pdfDao.getByPath(filePath)
} }
suspend fun getAllDocumentsOnce(): List<PdfDocumentEntity> = pdfDao.getAllDocumentsOnce()
fun getAllDocuments(): Flow<List<PdfDocumentEntity>> = pdfDao.getAllDocuments() fun getAllDocuments(): Flow<List<PdfDocumentEntity>> = pdfDao.getAllDocuments()
fun getFavoriteDocuments(): Flow<List<PdfDocumentEntity>> = pdfDao.getFavoriteDocuments() 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) { suspend fun updateFavoriteStatus(fileHash: String, isFavorite: Boolean) {
val document = pdfDao.getByHash(fileHash)?.copy( val document = pdfDao.getByHash(fileHash)?.copy(
@ -55,13 +58,32 @@ class PdfRepository private constructor(context: Context) {
document?.let { pdfDao.update(it) } 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) { suspend fun addToRecent(pdfHash: String, page: Int = 0) {
val existing = recentDao.getByPdfHash(pdfHash) val existing = recentDao.getByPdfHash(pdfHash)
if (existing != null) { if (existing != null) {
recentDao.updateOpenTime(pdfHash) recentDao.updateOpenTime(pdfHash)
} else { } 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 addBookmark(bookmark: BookmarkEntity): Long = bookmarkDao.insert(bookmark)
suspend fun updateBookmark(bookmark: BookmarkEntity) = bookmarkDao.update(bookmark) suspend fun updateBookmark(bookmark: BookmarkEntity) = bookmarkDao.update(bookmark)
suspend fun deleteBookmark(bookmark: BookmarkEntity) = bookmarkDao.delete(bookmark) suspend fun deleteBookmark(bookmark: BookmarkEntity) = bookmarkDao.delete(bookmark)
fun getBookmarksByPdf(pdfHash: String): Flow<List<BookmarkEntity>> = bookmarkDao.getBookmarksByPdf(pdfHash) fun getBookmarksByPdf(pdfHash: String): Flow<List<BookmarkEntity>> =
suspend fun getBookmarksByPage(pdfHash: String, page: Int): List<BookmarkEntity> = bookmarkDao.getBookmarksByPage(pdfHash, page) 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 addNote(note: NoteEntity): Long = noteDao.insert(note)
suspend fun updateNote(note: NoteEntity) = noteDao.update(note) suspend fun updateNote(note: NoteEntity) = noteDao.update(note)
suspend fun deleteNote(note: NoteEntity) = noteDao.delete(note) suspend fun deleteNote(note: NoteEntity) = noteDao.delete(note)
fun getNotesByPdf(pdfHash: String): Flow<List<NoteEntity>> = noteDao.getNotesByPdf(pdfHash) fun getNotesByPdf(pdfHash: String): Flow<List<NoteEntity>> = noteDao.getNotesByPdf(pdfHash)
suspend fun getNotesByPage(pdfHash: String, page: Int): List<NoteEntity> = noteDao.getNotesByPage(pdfHash, page) suspend fun getNotesByPage(pdfHash: String, page: Int): List<NoteEntity> =
fun getNotesByType(pdfHash: String, noteType: String): Flow<List<NoteEntity>> = noteDao.getNotesByType(pdfHash, noteType) noteDao.getNotesByPage(pdfHash, page)
fun getNotesByType(pdfHash: String, noteType: String): Flow<List<NoteEntity>> =
noteDao.getNotesByType(pdfHash, noteType)
// 组合查询 // 组合查询
suspend fun getPdfWithDetails(pdfHash: String): Flow<PdfDetails> { suspend fun getPdfWithDetails(pdfHash: String): Flow<PdfDetails> {
return combine( 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), bookmarkDao.getBookmarksByPdf(pdfHash),
noteDao.getNotesByPdf(pdfHash) noteDao.getNotesByPdf(pdfHash)
) { document, bookmarks, notes -> ) { document, bookmarks, notes ->
@ -137,7 +166,8 @@ class PdfRepository private constructor(context: Context) {
} }
fun getInstance(): PdfRepository { fun getInstance(): PdfRepository {
return INSTANCE ?: throw IllegalStateException("PdfRepository must be initialized first") return INSTANCE
?: throw IllegalStateException("PdfRepository must be initialized first")
} }
// 向后兼容的方法 // 向后兼容的方法

View File

@ -6,6 +6,7 @@ import android.view.View
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.all.pdfreader.pro.app.PDFReaderApplication
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityMainBinding import com.all.pdfreader.pro.app.databinding.ActivityMainBinding
import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment
@ -31,7 +32,6 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val pdfRepository = getRepository() private val pdfRepository = getRepository()
private lateinit var fileChangeObserver: FileChangeObserver
private lateinit var pdfScanner: PdfScanner private lateinit var pdfScanner: PdfScanner
private val homeFragment = HomeFrag() private val homeFragment = HomeFrag()
@ -64,23 +64,6 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
updateSelectedNav(activeFragment) 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() { private fun setupFragments() {
supportFragmentManager.beginTransaction().add(R.id.fragment_fl, toolsFragment, "TOOLS") supportFragmentManager.beginTransaction().add(R.id.fragment_fl, toolsFragment, "TOOLS")
.hide(toolsFragment).add(R.id.fragment_fl, favoriteFragment, "FAVORITE") .hide(toolsFragment).add(R.id.fragment_fl, favoriteFragment, "FAVORITE")
@ -161,7 +144,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
logDebug("main onResume") logDebug("main onResume")
if (StoragePermissionHelper.hasBasicStoragePermission(this)) { if (StoragePermissionHelper.hasBasicStoragePermission(this)) {
// 有授权才初始化文件变化监听器 // 有授权才初始化文件变化监听器
fileChangeObserver = FileChangeObserver(this, lifecycle) PDFReaderApplication.getInstance().startFileChangeObserving()
scanningStrategy() scanningStrategy()
binding.pnLayout.visibility = View.GONE binding.pnLayout.visibility = View.GONE
} else { } 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() { override fun onPermissionGranted() {
logDebug("main onPermissionGranted") logDebug("main onPermissionGranted")
//授权成功后:隐藏授权提示,开始扫描文件 //授权成功后:隐藏授权提示,开始扫描文件
binding.pnLayout.visibility = View.GONE binding.pnLayout.visibility = View.GONE
fileChangeObserver = FileChangeObserver(this, lifecycle) PDFReaderApplication.getInstance().startFileChangeObserving()
scanningStrategy() scanningStrategy()
} }

View File

@ -2,12 +2,14 @@ package com.all.pdfreader.pro.app.ui.act
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity 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.OnErrorListener
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener
import com.github.barteksc.pdfviewer.listener.OnPageChangeListener import com.github.barteksc.pdfviewer.listener.OnPageChangeListener
@ -18,37 +20,82 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
OnErrorListener { OnErrorListener {
override val TAG: String = "PdfViewActivity" 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 binding: ActivityPdfViewBinding
private lateinit var pdfDocument: PdfDocumentEntity 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityPdfViewBinding.inflate(layoutInflater) binding = ActivityPdfViewBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// 获取传递的PDF文档数据 val filePath = intent.getStringExtra(EXTRA_PDF_HASH)
pdfDocument = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ?: throw IllegalArgumentException("PDF file hash is required")
intent.getParcelableExtra(EXTRA_PDF_DOCUMENT, PdfDocumentEntity::class.java)
} else {
@Suppress("DEPRECATION") intent.getParcelableExtra<PdfDocumentEntity>(EXTRA_PDF_DOCUMENT)
} ?: throw IllegalArgumentException("PDF document data 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() { private fun loadPdf() {
logDebug("loadPdf ->${pdfDocument.lastReadPage} ${pdfDocument.readingProgress}")
// 使用传递的文件路径加载PDF // 使用传递的文件路径加载PDF
val file = File(pdfDocument.filePath) val file = File(pdfDocument.filePath)
if (file.exists()) { if (file.exists()) {
binding.pdfview val pdfView = binding.pdfview
.fromFile(file)
// 如果有密码,先尝试使用密码加载
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) // 从上次阅读页码开始 .defaultPage(pdfDocument.lastReadPage) // 从上次阅读页码开始
.enableDoubletap(true)// 是否允许双击缩放 .enableDoubletap(true)// 是否允许双击缩放
.onLoad(this) .onLoad(this)
.enableAnnotationRendering(true) // 是否渲染注释(如评论、颜色、表单等) .enableAnnotationRendering(true) // 是否渲染注释(如评论、颜色、表单等)
.onError(this) .onError(this)
.onPageChange(this) .onPageChange(this)
.scrollHandle(CustomScrollHandle(this))
.load() .load()
} else { } else {
showToast(getString(R.string.file_not) + ": ${pdfDocument.fileName}") showToast(getString(R.string.file_not) + ": ${pdfDocument.fileName}")
@ -63,38 +110,103 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList
//PDF 加载出错时回调 //PDF 加载出错时回调
override fun onError(t: Throwable?) { 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) { override fun onPageChanged(page: Int, pageCount: Int) {
// 保存阅读进度 // 保存阅读进度到内存
pdfDocument = pdfDocument.copy( pdfDocument = pdfDocument.copy(
lastReadPage = page, readingProgress = (page.toFloat() / pageCount.toFloat()) * 100 lastReadPage = page, readingProgress = (page.toFloat() / pageCount.toFloat()) * 100
) )
saveReadingProgress()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
saveReadingProgress()
} }
private fun saveReadingProgress() { private fun saveReadingProgress() {
lifecycleScope.launch { lifecycleScope.launch {
pdfRepository.updateReadingProgress( repository.updateReadingProgress(
pdfDocument.fileHash, pdfDocument.lastReadPage, pdfDocument.readingProgress pdfDocument.fileHash, pdfDocument.lastReadPage, pdfDocument.readingProgress
) )
} }
} }
companion object { private fun showPasswordDialog(file: File) {
private const val EXTRA_PDF_DOCUMENT = "extra_pdf_document" val builder = android.app.AlertDialog.Builder(this)
builder.setTitle("PDF密码保护")
builder.setMessage("请输入PDF文件密码")
// 创建启动Intent的便捷方法 val input = android.widget.EditText(this)
fun createIntent(context: Context, pdfDocument: PdfDocumentEntity): Intent { input.inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
return Intent(context, PdfViewActivity::class.java).apply { builder.setView(input)
putExtra(EXTRA_PDF_DOCUMENT, pdfDocument)
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) // 重新显示密码对话框
} }
} }
} }

View 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
}
}

View File

@ -38,7 +38,7 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment {
private fun initView() { private fun initView() {
adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf -> adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf ->
val intent = PdfViewActivity.createIntent(requireContext(), pdf) val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath)
startActivity(intent) startActivity(intent)
}, onMoreClick = { pdf -> }, onMoreClick = { pdf ->

View File

@ -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);
}
}

View File

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.all.pdfreader.pro.app.util package com.all.pdfreader.pro.app.util
import android.content.Context import android.content.Context
@ -9,20 +7,15 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
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 kotlin.collections.distinctBy
import kotlin.collections.plus
class FileChangeObserver( class FileChangeObserver(
private val context: Context, private val context: Context,
private val lifecycle: Lifecycle
) : LifecycleObserver { ) : LifecycleObserver {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@ -33,11 +26,6 @@ class FileChangeObserver(
private var lastScanTime = 0L private var lastScanTime = 0L
private val debounceDelay = 3000L // 3秒防抖 private val debounceDelay = 3000L // 3秒防抖
init {
lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startObserving() { fun startObserving() {
if (isObserving) return if (isObserving) return
@ -61,7 +49,6 @@ class FileChangeObserver(
} }
} }
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopObserving() { fun stopObserving() {
if (!isObserving) return if (!isObserving) return

View File

@ -2,11 +2,14 @@ package com.all.pdfreader.pro.app.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.pdf.PdfRenderer
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import java.io.File import java.io.File
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -313,4 +316,32 @@ object FileUtils {
data class FileInfo( data class FileInfo(
val name: String, val size: Long, val uri: Uri 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) {}
}
}
} }

View File

@ -7,42 +7,41 @@ import android.graphics.Color
import android.graphics.pdf.PdfRenderer 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 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import androidx.core.graphics.createBitmap
import com.github.barteksc.pdfviewer.PDFView
import java.util.concurrent.CountDownLatch
class PdfScanner( class PdfScanner(
private val context: Context, private val context: Context, private val pdfRepository: PdfRepository
private val pdfRepository: PdfRepository
) { ) {
private val TAG = "ocean-PdfScanner"
suspend fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) { suspend fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) {
if (!StoragePermissionHelper.hasBasicStoragePermission(context)) { if (!StoragePermissionHelper.hasBasicStoragePermission(context)) {
LogUtil.logDebug("ocean", "PdfScanner: 权限不足") LogUtil.logDebug(TAG, "PdfScanner: 权限不足")
callback.invoke(false) callback.invoke(false)
return return
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
LogUtil.logDebug("ocean", "PdfScanner: 🔍 开始扫描PDF文件...") LogUtil.logDebug(TAG, "PdfScanner: 🔍 开始扫描PDF文件...")
// 扫描应用私有目录(无需权限) // 扫描应用私有目录(无需权限)
val privateFiles = FileUtils.scanPdfFiles(context) val privateFiles = FileUtils.scanPdfFiles(context)
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG, "PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件"
"PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件"
) )
privateFiles.forEach { file -> privateFiles.forEach { file ->
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG,
"PdfScanner: 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})" "PdfScanner: 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})"
) )
} }
@ -50,41 +49,44 @@ class PdfScanner(
// 扫描MediaStore需要权限 // 扫描MediaStore需要权限
val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(context) val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(context)
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG, "PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件"
"PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件"
) )
mediaStoreFiles.forEach { file -> mediaStoreFiles.forEach { file ->
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG,
"PdfScanner: 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})" "PdfScanner: 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})"
) )
} }
// 合并并去重 // 合并并去重
val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath } val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath }
LogUtil.logDebug("ocean", "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件") LogUtil.logDebug(TAG, "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件")
// 处理每个PDF文件 // 处理每个PDF文件
allFiles.forEachIndexed { index, file -> allFiles.forEachIndexed { index, file ->
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG,
"PdfScanner: 🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name}" "PdfScanner: 🔄 处理文件 ${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("ocean", "PdfScanner: 🔑 文件哈希: $fileHash") LogUtil.logDebug(TAG, "PdfScanner: 🔑 文件哈希: $fileHash")
if (fileHash != null) { if (fileHash != null) {
val existingDoc = pdfRepository.getDocumentByHash(fileHash) val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath)
if (existingDoc == null) { if (existingDoc == null) {
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG, "PdfScanner: 🆕 发现新PDF文件: ${file.name}"
"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 = val metadata =
PdfMetadataExtractor.extractMetadata(file.absolutePath) PdfMetadataExtractor.extractMetadata(file.absolutePath)
val document = PdfDocumentEntity( val document = PdfDocumentEntity(
@ -100,28 +102,60 @@ class PdfScanner(
metadataSubject = metadata?.subject, metadataSubject = metadata?.subject,
metadataKeywords = metadata?.keywords, metadataKeywords = metadata?.keywords,
metadataCreationDate = metadata?.creationDate?.time, metadataCreationDate = metadata?.creationDate?.time,
metadataModificationDate = metadata?.modificationDate?.time metadataModificationDate = metadata?.modificationDate?.time,
isPassword = isPassword
) )
pdfRepository.insertOrUpdateDocument(document) pdfRepository.insertOrUpdateDocument(document)
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG, "PdfScanner: ✅ 已保存到数据库: ${file.name}"
"PdfScanner: ✅ 已保存到数据库: ${file.name}"
) )
} else { } else {
LogUtil.logDebug( LogUtil.logDebug(TAG, "PdfScanner: 📋 文件已存在: ${file.name}")
"ocean", // 🔹 文件已存在,检查是否需要更新
"PdfScanner: 📋 文件已存在: ${file.name}" var needUpdate = false
) var updatedDoc = existingDoc.copy()
if (existingDoc.filePath != file.absolutePath) {
LogUtil.logDebug( // 路径/修改时间更新
"ocean", if (existingDoc.filePath != file.absolutePath || existingDoc.lastModified != file.lastModified()) {
"PdfScanner: 🔄 更新文件路径: ${existingDoc.filePath} -> ${file.absolutePath}" LogUtil.logDebug(TAG, "PdfScanner: ✅ 路径/修改时间需要更新")
) updatedDoc = updatedDoc.copy(
val updatedDoc = existingDoc.copy(
filePath = file.absolutePath, filePath = file.absolutePath,
lastModified = file.lastModified() 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) 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 -> pdfRepository.getAllDocumentsOnce().forEach { doc ->
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)
LogUtil.logDebug( LogUtil.logDebug(
"ocean", TAG, "PdfScanner: 📖 ${doc.fileName} - ${doc.pageCount}页 - ${
"PdfScanner: ✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}" 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) { } catch (e: Exception) {
Log.e("ocean", "PdfScanner: ❌ 扫描出错: ${e.message}", e) Log.e(TAG, "PdfScanner: ❌ 扫描出错: ${e.message}", e)
callback.invoke(false) callback.invoke(false)
} }
} }

View File

@ -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)
}
}
}

View 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>

View 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>

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

View 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>

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

View File

@ -6,6 +6,7 @@
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="56dp"
android:gravity="center_vertical" android:gravity="center_vertical"
@ -41,4 +42,36 @@
</LinearLayout> </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> </LinearLayout>

View File

@ -39,7 +39,7 @@
android:id="@+id/tvFileName" android:id="@+id/tvFileName"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="marquee" android:ellipsize="middle"
android:maxLines="1" android:maxLines="1"
android:text="@string/app_name" android:text="@string/app_name"
android:textColor="@color/black" android:textColor="@color/black"

View 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>

View File

@ -24,4 +24,8 @@
<string name="permission_denied">Permission Denied</string> <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="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="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> </resources>