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.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -27,7 +27,10 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -10,25 +11,28 @@ import kotlinx.parcelize.Parcelize
|
|||||||
data class PdfDocumentEntity(
|
data class PdfDocumentEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val fileHash: String, // 文件内容哈希(MD5/SHA-1)
|
val fileHash: String, // 文件内容哈希(MD5/SHA-1)
|
||||||
|
|
||||||
val filePath: String, // 当前文件路径
|
val filePath: String, // 当前文件路径
|
||||||
val fileName: String, // 文件名
|
val fileName: String, // 文件名
|
||||||
val fileSize: Long, // 文件大小(字节)
|
val fileSize: Long, // 文件大小(字节)
|
||||||
val lastModified: Long, // 文件最后修改时间
|
val lastModified: Long, // 文件最后修改时间
|
||||||
val pageCount: Int, // 总页数
|
val pageCount: Int, // 总页数
|
||||||
|
|
||||||
val isFavorite: Boolean = false, // 是否收藏
|
val isFavorite: Boolean = false, // 是否收藏
|
||||||
val addedToFavoriteTime: Long? = null, // 收藏时间
|
val addedToFavoriteTime: Long? = null, // 收藏时间
|
||||||
|
|
||||||
val lastOpenedTime: Long = 0, // 最后打开时间
|
val lastOpenedTime: Long = 0, // 最后打开时间
|
||||||
val lastReadPage: Int = 0, // 最后阅读页码
|
val lastReadPage: Int = 0, // 最后阅读页码
|
||||||
val readingProgress: Float = 0f, // 阅读进度(0-100)
|
val readingProgress: Float = 0f, // 阅读进度(0-100)
|
||||||
|
|
||||||
val thumbnailPath: String? = null, // 缩略图本地路径
|
val thumbnailPath: String? = null, // 缩略图本地路径
|
||||||
val metadataTitle: String? = null, // PDF元数据标题
|
val metadataTitle: String? = null, // PDF元数据标题
|
||||||
val metadataAuthor: String? = null, // PDF元数据作者
|
val metadataAuthor: String? = null, // PDF元数据作者
|
||||||
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
|
||||||
@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.combine
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
class PdfRepository private constructor(context: Context) {
|
class PdfRepository private constructor(context: Context) {
|
||||||
|
|
||||||
private val database = Room.databaseBuilder(
|
private val database = Room.databaseBuilder(
|
||||||
context,
|
context,
|
||||||
PdfDatabase::class.java,
|
PdfDatabase::class.java,
|
||||||
@ -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 ->
|
||||||
@ -98,7 +127,7 @@ class PdfRepository private constructor(context: Context) {
|
|||||||
return try {
|
return try {
|
||||||
val file = java.io.File(filePath)
|
val file = java.io.File(filePath)
|
||||||
if (!file.exists()) return ""
|
if (!file.exists()) return ""
|
||||||
|
|
||||||
val digest = MessageDigest.getInstance("MD5")
|
val digest = MessageDigest.getInstance("MD5")
|
||||||
file.inputStream().use { input ->
|
file.inputStream().use { input ->
|
||||||
val buffer = ByteArray(8192)
|
val buffer = ByteArray(8192)
|
||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向后兼容的方法
|
// 向后兼容的方法
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -17,38 +19,83 @@ import java.io.File
|
|||||||
class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeListener,
|
class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeListener,
|
||||||
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密码保护")
|
||||||
// 创建启动Intent的便捷方法
|
builder.setMessage("请输入PDF文件密码:")
|
||||||
fun createIntent(context: Context, pdfDocument: PdfDocumentEntity): Intent {
|
|
||||||
return Intent(context, PdfViewActivity::class.java).apply {
|
val input = android.widget.EditText(this)
|
||||||
putExtra(EXTRA_PDF_DOCUMENT, pdfDocument)
|
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() {
|
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 ->
|
||||||
|
|||||||
@ -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
|
package com.all.pdfreader.pro.app.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -9,45 +7,35 @@ 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())
|
||||||
private var contentObserver: ContentObserver? = null
|
private var contentObserver: ContentObserver? = null
|
||||||
private var isObserving = false
|
private var isObserving = false
|
||||||
|
|
||||||
// 防抖机制,避免频繁触发
|
// 防抖机制,避免频繁触发
|
||||||
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
|
||||||
|
|
||||||
contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
super.onChange(selfChange, uri)
|
super.onChange(selfChange, uri)
|
||||||
handleFileChange()
|
handleFileChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.contentResolver.registerContentObserver(
|
context.contentResolver.registerContentObserver(
|
||||||
MediaStore.Files.getContentUri("external"),
|
MediaStore.Files.getContentUri("external"),
|
||||||
@ -60,11 +48,10 @@ class FileChangeObserver(
|
|||||||
Log.e("ocean", "❌ 注册文件变化监听器失败", e)
|
Log.e("ocean", "❌ 注册文件变化监听器失败", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
|
||||||
fun stopObserving() {
|
fun stopObserving() {
|
||||||
if (!isObserving) return
|
if (!isObserving) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contentObserver?.let { observer ->
|
contentObserver?.let { observer ->
|
||||||
context.contentResolver.unregisterContentObserver(observer)
|
context.contentResolver.unregisterContentObserver(observer)
|
||||||
@ -75,21 +62,21 @@ class FileChangeObserver(
|
|||||||
Log.e("ocean", "❌ 注销文件变化监听器失败", e)
|
Log.e("ocean", "❌ 注销文件变化监听器失败", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFileChange() {
|
private fun handleFileChange() {
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
if (currentTime - lastScanTime < debounceDelay) {
|
if (currentTime - lastScanTime < debounceDelay) {
|
||||||
return // 防抖
|
return // 防抖
|
||||||
}
|
}
|
||||||
|
|
||||||
lastScanTime = currentTime
|
lastScanTime = currentTime
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(debounceDelay) // 额外延迟,避免频繁扫描
|
delay(debounceDelay) // 额外延迟,避免频繁扫描
|
||||||
|
|
||||||
if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
|
if (StoragePermissionHelper.hasBasicStoragePermission(context)) {
|
||||||
Log.d("ocean", "📂 检测到文件变化,可以执行增量扫描...")
|
Log.d("ocean", "📂 检测到文件变化,可以执行增量扫描...")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
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>
|
||||||
@ -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"
|
||||||
|
|||||||
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="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>
|
||||||
Loading…
Reference in New Issue
Block a user