diff --git a/app/src/main/java/com/all/pdfreader/pro/app/PDFReaderApplication.kt b/app/src/main/java/com/all/pdfreader/pro/app/PDFReaderApplication.kt index ab51d65..9440544 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/PDFReaderApplication.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/PDFReaderApplication.kt @@ -3,24 +3,31 @@ package com.all.pdfreader.pro.app import android.app.Application import android.content.Context import com.all.pdfreader.pro.app.room.repository.PdfRepository +import com.all.pdfreader.pro.app.util.FileChangeObserver class PDFReaderApplication : Application() { companion object { private lateinit var instance: PDFReaderApplication - fun getInstance(): PDFReaderApplication = instance - fun getContext(): Context = instance.applicationContext } + private lateinit var fileChangeObserver: FileChangeObserver + override fun onCreate() { super.onCreate() instance = this + // 初始化文件变化监听(不立即启动) + fileChangeObserver = FileChangeObserver(this) + // 初始化数据库 PdfRepository.initialize(this) } - -} \ No newline at end of file + // 在权限授权后调用 + fun startFileChangeObserving() { + fileChangeObserver.startObserving() + } +} diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/dao/PdfDocumentDao.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/PdfDocumentDao.kt index fe93834..16f90c5 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/room/dao/PdfDocumentDao.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/room/dao/PdfDocumentDao.kt @@ -27,7 +27,10 @@ interface PdfDocumentDao { @Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC") fun getAllDocuments(): Flow> - + + @Query("SELECT * FROM pdf_documents ORDER BY lastOpenedTime DESC") + suspend fun getAllDocumentsOnce(): List + @Delete suspend fun delete(document: PdfDocumentEntity) diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/entity/PdfDocumentEntity.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/PdfDocumentEntity.kt index a91933d..6717fa9 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/room/entity/PdfDocumentEntity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/room/entity/PdfDocumentEntity.kt @@ -3,6 +3,7 @@ package com.all.pdfreader.pro.app.room.entity import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey +import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordDialog import kotlinx.parcelize.Parcelize @Parcelize @@ -10,25 +11,28 @@ import kotlinx.parcelize.Parcelize data class PdfDocumentEntity( @PrimaryKey val fileHash: String, // 文件内容哈希(MD5/SHA-1) - + val filePath: String, // 当前文件路径 val fileName: String, // 文件名 val fileSize: Long, // 文件大小(字节) val lastModified: Long, // 文件最后修改时间 val pageCount: Int, // 总页数 - + val isFavorite: Boolean = false, // 是否收藏 val addedToFavoriteTime: Long? = null, // 收藏时间 - + val lastOpenedTime: Long = 0, // 最后打开时间 val lastReadPage: Int = 0, // 最后阅读页码 val readingProgress: Float = 0f, // 阅读进度(0-100) - + val thumbnailPath: String? = null, // 缩略图本地路径 val metadataTitle: String? = null, // PDF元数据标题 val metadataAuthor: String? = null, // PDF元数据作者 val metadataSubject: String? = null, // PDF元数据主题 val metadataKeywords: String? = null, // PDF元数据关键词 val metadataCreationDate: Long? = null, // PDF创建时间 - val metadataModificationDate: Long? = null // PDF修改时间 -): Parcelable \ No newline at end of file + val metadataModificationDate: Long? = null, // PDF修改时间 + + val password: String? = null,// PDF密码(加密存储) + val isPassword: Boolean = false//是否存在密码 +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/room/repository/PdfRepository.kt b/app/src/main/java/com/all/pdfreader/pro/app/room/repository/PdfRepository.kt index 11ab84e..91f42bf 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/room/repository/PdfRepository.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/room/repository/PdfRepository.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.combine import java.security.MessageDigest class PdfRepository private constructor(context: Context) { - + private val database = Room.databaseBuilder( context, PdfDatabase::class.java, @@ -34,9 +34,12 @@ class PdfRepository private constructor(context: Context) { return pdfDao.getByPath(filePath) } + suspend fun getAllDocumentsOnce(): List = pdfDao.getAllDocumentsOnce() + fun getAllDocuments(): Flow> = pdfDao.getAllDocuments() fun getFavoriteDocuments(): Flow> = pdfDao.getFavoriteDocuments() - fun searchDocuments(query: String): Flow> = pdfDao.searchDocuments(query) + fun searchDocuments(query: String): Flow> = + pdfDao.searchDocuments(query) suspend fun updateFavoriteStatus(fileHash: String, isFavorite: Boolean) { val document = pdfDao.getByHash(fileHash)?.copy( @@ -55,13 +58,32 @@ class PdfRepository private constructor(context: Context) { document?.let { pdfDao.update(it) } } + suspend fun updatePasswordStatus(fileHash: String, isPassword: Boolean) { + val document = pdfDao.getByHash(fileHash)?.copy( + isPassword = isPassword + ) + document?.let { pdfDao.update(it) } + } + + suspend fun updatePassword(fileHash: String, password: String?) { + val document = pdfDao.getByHash(fileHash)?.copy( + password = password + ) + document?.let { pdfDao.update(it) } + } + // 最近阅读相关操作 suspend fun addToRecent(pdfHash: String, page: Int = 0) { val existing = recentDao.getByPdfHash(pdfHash) if (existing != null) { recentDao.updateOpenTime(pdfHash) } else { - recentDao.insertOrUpdate(RecentReadEntity(pdfHash = pdfHash, lastOpenedTime = System.currentTimeMillis())) + recentDao.insertOrUpdate( + RecentReadEntity( + pdfHash = pdfHash, + lastOpenedTime = System.currentTimeMillis() + ) + ) } } @@ -71,21 +93,28 @@ class PdfRepository private constructor(context: Context) { suspend fun addBookmark(bookmark: BookmarkEntity): Long = bookmarkDao.insert(bookmark) suspend fun updateBookmark(bookmark: BookmarkEntity) = bookmarkDao.update(bookmark) suspend fun deleteBookmark(bookmark: BookmarkEntity) = bookmarkDao.delete(bookmark) - fun getBookmarksByPdf(pdfHash: String): Flow> = bookmarkDao.getBookmarksByPdf(pdfHash) - suspend fun getBookmarksByPage(pdfHash: String, page: Int): List = bookmarkDao.getBookmarksByPage(pdfHash, page) + fun getBookmarksByPdf(pdfHash: String): Flow> = + bookmarkDao.getBookmarksByPdf(pdfHash) + + suspend fun getBookmarksByPage(pdfHash: String, page: Int): List = + bookmarkDao.getBookmarksByPage(pdfHash, page) // 注释相关操作 suspend fun addNote(note: NoteEntity): Long = noteDao.insert(note) suspend fun updateNote(note: NoteEntity) = noteDao.update(note) suspend fun deleteNote(note: NoteEntity) = noteDao.delete(note) fun getNotesByPdf(pdfHash: String): Flow> = noteDao.getNotesByPdf(pdfHash) - suspend fun getNotesByPage(pdfHash: String, page: Int): List = noteDao.getNotesByPage(pdfHash, page) - fun getNotesByType(pdfHash: String, noteType: String): Flow> = noteDao.getNotesByType(pdfHash, noteType) + suspend fun getNotesByPage(pdfHash: String, page: Int): List = + noteDao.getNotesByPage(pdfHash, page) + + fun getNotesByType(pdfHash: String, noteType: String): Flow> = + noteDao.getNotesByType(pdfHash, noteType) // 组合查询 suspend fun getPdfWithDetails(pdfHash: String): Flow { return combine( - pdfDao.getByHash(pdfHash)?.let { kotlinx.coroutines.flow.flowOf(it) } ?: kotlinx.coroutines.flow.flowOf(null), + pdfDao.getByHash(pdfHash)?.let { kotlinx.coroutines.flow.flowOf(it) } + ?: kotlinx.coroutines.flow.flowOf(null), bookmarkDao.getBookmarksByPdf(pdfHash), noteDao.getNotesByPdf(pdfHash) ) { document, bookmarks, notes -> @@ -98,7 +127,7 @@ class PdfRepository private constructor(context: Context) { return try { val file = java.io.File(filePath) if (!file.exists()) return "" - + val digest = MessageDigest.getInstance("MD5") file.inputStream().use { input -> val buffer = ByteArray(8192) @@ -137,7 +166,8 @@ class PdfRepository private constructor(context: Context) { } fun getInstance(): PdfRepository { - return INSTANCE ?: throw IllegalStateException("PdfRepository must be initialized first") + return INSTANCE + ?: throw IllegalStateException("PdfRepository must be initialized first") } // 向后兼容的方法 diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt index f340e63..e46555e 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt @@ -6,6 +6,7 @@ import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import com.all.pdfreader.pro.app.PDFReaderApplication import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.databinding.ActivityMainBinding import com.all.pdfreader.pro.app.ui.dialog.PermissionDialogFragment @@ -31,7 +32,6 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback private lateinit var binding: ActivityMainBinding private val pdfRepository = getRepository() - private lateinit var fileChangeObserver: FileChangeObserver private lateinit var pdfScanner: PdfScanner private val homeFragment = HomeFrag() @@ -64,23 +64,6 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback updateSelectedNav(activeFragment) } - private fun scanningStrategy() { - // 智能扫描策略 - if (pdfScanner.shouldScan()) { - logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)") - if (StoragePermissionHelper.hasBasicStoragePermission(this)) { - lifecycleScope.launch { - pdfScanner.scanAndLoadPdfFiles() - } - } else { - logDebug("❌ 权限不足,跳过扫描") - } - } else { - val hoursAgo = pdfScanner.getHoursSinceLastScan() - logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前") - } - } - private fun setupFragments() { supportFragmentManager.beginTransaction().add(R.id.fragment_fl, toolsFragment, "TOOLS") .hide(toolsFragment).add(R.id.fragment_fl, favoriteFragment, "FAVORITE") @@ -161,7 +144,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback logDebug("main onResume") if (StoragePermissionHelper.hasBasicStoragePermission(this)) { // 有授权才初始化文件变化监听器 - fileChangeObserver = FileChangeObserver(this, lifecycle) + PDFReaderApplication.getInstance().startFileChangeObserving() scanningStrategy() binding.pnLayout.visibility = View.GONE } else { @@ -174,12 +157,29 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback } } + private fun scanningStrategy() { + // 智能扫描策略 + if (pdfScanner.shouldScan()) { + logDebug("🔄 需要扫描PDF文件 (首次启动或超过24小时)") + if (StoragePermissionHelper.hasBasicStoragePermission(this)) { + lifecycleScope.launch { + pdfScanner.scanAndLoadPdfFiles() + } + } else { + logDebug("❌ 权限不足,跳过扫描") + } + } else { + val hoursAgo = pdfScanner.getHoursSinceLastScan() + logDebug("⏭️ 跳过扫描,上次扫描在${hoursAgo}小时前") + } + } + // 授权后续操作 override fun onPermissionGranted() { logDebug("main onPermissionGranted") //授权成功后:隐藏授权提示,开始扫描文件 binding.pnLayout.visibility = View.GONE - fileChangeObserver = FileChangeObserver(this, lifecycle) + PDFReaderApplication.getInstance().startFileChangeObserving() scanningStrategy() } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt index e941b8f..e7bc73e 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt @@ -2,12 +2,14 @@ package com.all.pdfreader.pro.app.ui.act import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity +import com.all.pdfreader.pro.app.ui.view.CustomScrollHandle +import com.all.pdfreader.pro.app.viewmodel.PdfViewModel import com.github.barteksc.pdfviewer.listener.OnErrorListener import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener import com.github.barteksc.pdfviewer.listener.OnPageChangeListener @@ -17,38 +19,83 @@ import java.io.File class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeListener, OnErrorListener { override val TAG: String = "PdfViewActivity" + + companion object { + private const val EXTRA_PDF_HASH = "extra_pdf_hash" + + // 创建启动Intent的便捷方法 + fun createIntent(context: Context, filePath: String): Intent { + return Intent(context, PdfViewActivity::class.java).apply { + putExtra(EXTRA_PDF_HASH, filePath) + } + } + } private lateinit var binding: ActivityPdfViewBinding private lateinit var pdfDocument: PdfDocumentEntity - private val pdfRepository = getRepository() - + private val viewModel by lazy { ViewModelProvider(this)[PdfViewModel::class.java] } + private val repository = getRepository() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityPdfViewBinding.inflate(layoutInflater) setContentView(binding.root) - // 获取传递的PDF文档数据 - pdfDocument = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(EXTRA_PDF_DOCUMENT, PdfDocumentEntity::class.java) - } else { - @Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_PDF_DOCUMENT) - } ?: throw IllegalArgumentException("PDF document data is required") + val filePath = intent.getStringExtra(EXTRA_PDF_HASH) + ?: throw IllegalArgumentException("PDF file hash is required") - loadPdf() + // 观察PDF文档数据 + viewModel.pdfDocument.observe(this) { document -> + document?.let { + pdfDocument = it + loadPdf() + } ?: run { + showToast(getString(R.string.file_not)) + finish() + } + } + + // 加载PDF数据 + viewModel.getPDFDocument(filePath) } private fun loadPdf() { + logDebug("loadPdf ->${pdfDocument.lastReadPage} ${pdfDocument.readingProgress}") // 使用传递的文件路径加载PDF val file = File(pdfDocument.filePath) if (file.exists()) { - binding.pdfview - .fromFile(file) + val pdfView = binding.pdfview + + // 如果有密码,先尝试使用密码加载 + if (!pdfDocument.password.isNullOrEmpty()) { + try { + pdfView.fromFile(file) + .password(pdfDocument.password) // 使用密码加载 + .defaultPage(pdfDocument.lastReadPage) + .enableDoubletap(true) + .onLoad(this) + .enableAnnotationRendering(true) + .onError(this) + .onPageChange(this) + .scrollHandle(CustomScrollHandle(this)) + .load() + return + } catch (e: Exception) { + logDebug("Password protected PDF failed: ${e.message}") + // 密码错误,显示密码输入对话框 + showPasswordDialog(file) + return + } + } + + // 无密码PDF正常加载 + pdfView.fromFile(file) .defaultPage(pdfDocument.lastReadPage) // 从上次阅读页码开始 .enableDoubletap(true)// 是否允许双击缩放 .onLoad(this) .enableAnnotationRendering(true) // 是否渲染注释(如评论、颜色、表单等) .onError(this) .onPageChange(this) + .scrollHandle(CustomScrollHandle(this)) .load() } else { showToast(getString(R.string.file_not) + ": ${pdfDocument.fileName}") @@ -63,38 +110,103 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList //PDF 加载出错时回调 override fun onError(t: Throwable?) { - + logDebug("PDF loading error: ${t?.message}") + t?.let { + val errorMessage = it.message ?: "未知错误" + + // 检查是否是密码相关的错误 + if (errorMessage.contains("Password") || errorMessage.contains("password")) { + // 如果当前没有设置密码,显示密码输入对话框 + if (pdfDocument.password.isNullOrEmpty()) { + val file = File(pdfDocument.filePath) + showPasswordDialog(file) + } else { + // 密码错误,显示密码输入对话框 + showToast("PDF密码错误") + val file = File(pdfDocument.filePath) + showPasswordDialog(file) + } + } else { + // 其他错误 + showToast("PDF加载失败: $errorMessage") + finish() + } + } } //页面切换时回调 override fun onPageChanged(page: Int, pageCount: Int) { - // 保存阅读进度 + // 保存阅读进度到内存 pdfDocument = pdfDocument.copy( lastReadPage = page, readingProgress = (page.toFloat() / pageCount.toFloat()) * 100 ) + saveReadingProgress() } override fun onDestroy() { super.onDestroy() - saveReadingProgress() } private fun saveReadingProgress() { lifecycleScope.launch { - pdfRepository.updateReadingProgress( + repository.updateReadingProgress( pdfDocument.fileHash, pdfDocument.lastReadPage, pdfDocument.readingProgress ) } } - companion object { - private const val EXTRA_PDF_DOCUMENT = "extra_pdf_document" - - // 创建启动Intent的便捷方法 - fun createIntent(context: Context, pdfDocument: PdfDocumentEntity): Intent { - return Intent(context, PdfViewActivity::class.java).apply { - putExtra(EXTRA_PDF_DOCUMENT, pdfDocument) + private fun showPasswordDialog(file: File) { + val builder = android.app.AlertDialog.Builder(this) + builder.setTitle("PDF密码保护") + builder.setMessage("请输入PDF文件密码:") + + val input = android.widget.EditText(this) + input.inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + builder.setView(input) + + builder.setPositiveButton("确定") { dialog, _ -> + val password = input.text.toString() + if (password.isNotEmpty()) { + // 尝试使用输入的密码重新加载PDF + tryLoadPdfWithPassword(file, password) + } else { + showToast("密码不能为空") } + dialog.dismiss() + } + + builder.setNegativeButton("取消") { dialog, _ -> + dialog.cancel() + finish() + } + + builder.setOnCancelListener { + finish() + } + + builder.show() + } + + private fun tryLoadPdfWithPassword(file: File, password: String) { + try { + binding.pdfview.fromFile(file) + .password(password) // 使用输入的密码 + .defaultPage(pdfDocument.lastReadPage) + .enableDoubletap(true) + .onLoad(this) + .enableAnnotationRendering(true) + .onError { error -> + logDebug("Password still incorrect: ${error?.message}") + showToast("密码错误,请重试") + showPasswordDialog(file) // 重新显示密码对话框 + } + .onPageChange(this) + .scrollHandle(CustomScrollHandle(this)) + .load() + } catch (e: Exception) { + logDebug("Failed to load PDF with provided password: ${e.message}") + showToast("密码错误,请重试") + showPasswordDialog(file) // 重新显示密码对话框 } } } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PdfPasswordDialog.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PdfPasswordDialog.kt new file mode 100644 index 0000000..294e1ee --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PdfPasswordDialog.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt index c0912f9..e33b7e8 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/HomeFrag.kt @@ -38,7 +38,7 @@ class HomeFrag : BaseFrag(), MainActivity.SortableFragment { private fun initView() { adapter = PdfAdapter(pdfList = mutableListOf(), onItemClick = { pdf -> - val intent = PdfViewActivity.createIntent(requireContext(), pdf) + val intent = PdfViewActivity.createIntent(requireContext(), pdf.filePath) startActivity(intent) }, onMoreClick = { pdf -> diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/view/CustomScrollHandle.java b/app/src/main/java/com/all/pdfreader/pro/app/ui/view/CustomScrollHandle.java new file mode 100644 index 0000000..e61a3b2 --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/view/CustomScrollHandle.java @@ -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); + } +} diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/FileChangeObserver.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/FileChangeObserver.kt index a99b29e..a5dbd75 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/FileChangeObserver.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/FileChangeObserver.kt @@ -1,5 +1,3 @@ -@file:Suppress("DEPRECATION") - package com.all.pdfreader.pro.app.util import android.content.Context @@ -9,45 +7,35 @@ import android.os.Handler import android.os.Looper import android.provider.MediaStore import android.util.Log -import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlin.collections.distinctBy -import kotlin.collections.plus class FileChangeObserver( private val context: Context, - private val lifecycle: Lifecycle ) : LifecycleObserver { - + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var contentObserver: ContentObserver? = null private var isObserving = false - + // 防抖机制,避免频繁触发 private var lastScanTime = 0L private val debounceDelay = 3000L // 3秒防抖 - - init { - lifecycle.addObserver(this) - } - - @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun startObserving() { if (isObserving) return - + contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) handleFileChange() } } - + try { context.contentResolver.registerContentObserver( MediaStore.Files.getContentUri("external"), @@ -60,11 +48,10 @@ class FileChangeObserver( Log.e("ocean", "❌ 注册文件变化监听器失败", e) } } - - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + fun stopObserving() { if (!isObserving) return - + try { contentObserver?.let { observer -> context.contentResolver.unregisterContentObserver(observer) @@ -75,21 +62,21 @@ class FileChangeObserver( Log.e("ocean", "❌ 注销文件变化监听器失败", e) } } - + private fun handleFileChange() { val currentTime = System.currentTimeMillis() if (currentTime - lastScanTime < debounceDelay) { return // 防抖 } - + lastScanTime = currentTime - + scope.launch { delay(debounceDelay) // 额外延迟,避免频繁扫描 - + if (StoragePermissionHelper.hasBasicStoragePermission(context)) { Log.d("ocean", "📂 检测到文件变化,可以执行增量扫描...") - + } } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt index 8510eb0..f757861 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt @@ -2,11 +2,14 @@ package com.all.pdfreader.pro.app.util import android.annotation.SuppressLint import android.content.Context +import android.graphics.pdf.PdfRenderer import android.net.Uri +import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.provider.OpenableColumns import android.util.Log import java.io.File +import java.io.IOException import java.io.InputStream import java.security.MessageDigest import java.text.SimpleDateFormat @@ -313,4 +316,32 @@ object FileUtils { data class FileInfo( val name: String, val size: Long, val uri: Uri ) + + /** + * 判断 PDF 是否加密(基于原生 PdfRenderer) + * + * @param file 要检测的 PDF 文件 + * @return true = 已加密 / 不能打开,false = 未加密 + */ + fun isPdfEncrypted(file: File): Boolean { + var fd: ParcelFileDescriptor? = null + var renderer: PdfRenderer? = null + return try { + fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + renderer = PdfRenderer(fd) + // 能成功打开 => 没有加密 + false + } catch (e: SecurityException) { + // PdfRenderer 在遇到加密 PDF 时可能抛 SecurityException + true + } catch (e: IOException) { + // 某些加密 PDF 也可能走 IOException + e.message?.contains("password", ignoreCase = true) == true + } finally { + try { + renderer?.close() + fd?.close() + } catch (_: Exception) {} + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt index e27ca57..89d4c2a 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt @@ -7,42 +7,41 @@ import android.graphics.Color import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor import android.util.Log +import androidx.core.graphics.createBitmap import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.repository.PdfRepository +import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream import java.util.concurrent.TimeUnit -import androidx.core.graphics.createBitmap -import com.github.barteksc.pdfviewer.PDFView -import java.util.concurrent.CountDownLatch class PdfScanner( - private val context: Context, - private val pdfRepository: PdfRepository + private val context: Context, private val pdfRepository: PdfRepository ) { + private val TAG = "ocean-PdfScanner" + suspend fun scanAndLoadPdfFiles(callback: (Boolean) -> Unit = {}) { if (!StoragePermissionHelper.hasBasicStoragePermission(context)) { - LogUtil.logDebug("ocean", "PdfScanner: 权限不足") + LogUtil.logDebug(TAG, "PdfScanner: 权限不足") callback.invoke(false) return } withContext(Dispatchers.IO) { try { - LogUtil.logDebug("ocean", "PdfScanner: 🔍 开始扫描PDF文件...") + LogUtil.logDebug(TAG, "PdfScanner: 🔍 开始扫描PDF文件...") // 扫描应用私有目录(无需权限) val privateFiles = FileUtils.scanPdfFiles(context) LogUtil.logDebug( - "ocean", - "PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件" + TAG, "PdfScanner: 📁 应用私有目录找到: ${privateFiles.size} 个PDF文件" ) privateFiles.forEach { file -> LogUtil.logDebug( - "ocean", + TAG, "PdfScanner: 📄 ${file.name} (${FileUtils.formatFileSize(file.length())})" ) } @@ -50,41 +49,44 @@ class PdfScanner( // 扫描MediaStore(需要权限) val mediaStoreFiles = FileUtils.scanPdfFilesFromMediaStore(context) LogUtil.logDebug( - "ocean", - "PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件" + TAG, "PdfScanner: 📱 MediaStore找到: ${mediaStoreFiles.size} 个PDF文件" ) mediaStoreFiles.forEach { file -> LogUtil.logDebug( - "ocean", + TAG, "PdfScanner: 📱 ${file.name} (${FileUtils.formatFileSize(file.length())})" ) } // 合并并去重 val allFiles = (privateFiles + mediaStoreFiles).distinctBy { it.absolutePath } - LogUtil.logDebug("ocean", "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件") + LogUtil.logDebug(TAG, "PdfScanner: 📊 总计扫描到: ${allFiles.size} 个PDF文件") // 处理每个PDF文件 allFiles.forEachIndexed { index, file -> LogUtil.logDebug( - "ocean", - "PdfScanner: 🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name}" + TAG, + "PdfScanner: 🔄 处理文件 ${index + 1}/${allFiles.size}: ${file.name} - ${file.absolutePath}" ) if (FileUtils.isPdfFile(file)) { val fileHash = FileUtils.calculateFileHash(file.absolutePath) - LogUtil.logDebug("ocean", "PdfScanner: 🔑 文件哈希: $fileHash") + LogUtil.logDebug(TAG, "PdfScanner: 🔑 文件哈希: $fileHash") if (fileHash != null) { - val existingDoc = pdfRepository.getDocumentByHash(fileHash) + val existingDoc = pdfRepository.getDocumentByPath(file.absolutePath) if (existingDoc == null) { LogUtil.logDebug( - "ocean", - "PdfScanner: 🆕 发现新PDF文件: ${file.name}" + TAG, "PdfScanner: 🆕 发现新PDF文件: ${file.name}" ) - val thumbnailPath = generateThumbnail(context, file) - + var thumbnailPath: String? = null + val isPassword = isPdfEncrypted(file) + LogUtil.logDebug(TAG, "PdfScanner: isPassword->${isPassword}") + if (!isPassword) {//没有密码的情况下才去获取缩略图 + thumbnailPath = generateThumbnail(context, file) ?: "" + } + LogUtil.logDebug(TAG, "PdfScanner: thumbnailPath->${thumbnailPath}") val metadata = PdfMetadataExtractor.extractMetadata(file.absolutePath) val document = PdfDocumentEntity( @@ -100,28 +102,60 @@ class PdfScanner( metadataSubject = metadata?.subject, metadataKeywords = metadata?.keywords, metadataCreationDate = metadata?.creationDate?.time, - metadataModificationDate = metadata?.modificationDate?.time + metadataModificationDate = metadata?.modificationDate?.time, + isPassword = isPassword ) pdfRepository.insertOrUpdateDocument(document) LogUtil.logDebug( - "ocean", - "PdfScanner: ✅ 已保存到数据库: ${file.name}" + TAG, "PdfScanner: ✅ 已保存到数据库: ${file.name}" ) } else { - LogUtil.logDebug( - "ocean", - "PdfScanner: 📋 文件已存在: ${file.name}" - ) - if (existingDoc.filePath != file.absolutePath) { - LogUtil.logDebug( - "ocean", - "PdfScanner: 🔄 更新文件路径: ${existingDoc.filePath} -> ${file.absolutePath}" - ) - val updatedDoc = existingDoc.copy( + LogUtil.logDebug(TAG, "PdfScanner: 📋 文件已存在: ${file.name}") + // 🔹 文件已存在,检查是否需要更新 + var needUpdate = false + var updatedDoc = existingDoc.copy() + + // 路径/修改时间更新 + if (existingDoc.filePath != file.absolutePath || existingDoc.lastModified != file.lastModified()) { + LogUtil.logDebug(TAG, "PdfScanner: ✅ 路径/修改时间需要更新") + updatedDoc = updatedDoc.copy( filePath = file.absolutePath, lastModified = file.lastModified() ) + needUpdate = true + } + + // 是否加密更新 + val currentIsPassword = isPdfEncrypted(file) + if (existingDoc.isPassword != currentIsPassword) { + LogUtil.logDebug(TAG, "PdfScanner: ✅ 密码状态需要更新") + updatedDoc = updatedDoc.copy(isPassword = currentIsPassword) + needUpdate = true + } + + if (!currentIsPassword) { + // 如果不是加密 PDF,再生成缩略图 + val newThumbnail = generateThumbnail(context, file) + if (existingDoc.thumbnailPath != newThumbnail) { + LogUtil.logDebug(TAG, "PdfScanner: ✅ 缩略图需要更新") + updatedDoc = updatedDoc.copy(thumbnailPath = newThumbnail) + needUpdate = true + } + } else { + updatedDoc = updatedDoc.copy(thumbnailPath = null) + needUpdate = true + } + + // 执行更新 + if (needUpdate) { pdfRepository.insertOrUpdateDocument(updatedDoc) + LogUtil.logDebug( + TAG, "PdfScanner: ✅ 数据库已更新: ${file.name}" + ) + } else { + LogUtil.logDebug( + TAG, "PdfScanner: ⏩ 无需更新: ${file.name}" + ) } } } @@ -129,31 +163,27 @@ class PdfScanner( } // 打印数据库中的总记录数 - pdfRepository.getAllDocuments().collect { docs -> - LogUtil.logDebug("ocean", "PdfScanner: 📊 数据库中共有: ${docs.size} 个PDF记录") - docs.forEach { doc -> - LogUtil.logDebug( - "ocean", - "PdfScanner: 📖 ${doc.fileName} - ${doc.pageCount}页 - ${ - FileUtils.formatFileSize( - doc.fileSize - ) - } - ${doc.thumbnailPath}" - ) - } - - // 标记扫描完成 - ScanManager.markScanComplete(context) - val lastScanTime = ScanManager.getLastScanTime(context) + pdfRepository.getAllDocumentsOnce().forEach { doc -> LogUtil.logDebug( - "ocean", - "PdfScanner: ✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}" + TAG, "PdfScanner: 📖 ${doc.fileName} - ${doc.pageCount}页 - ${ + FileUtils.formatFileSize( + doc.fileSize + ) + } - ${doc.thumbnailPath}" ) - callback.invoke(true) } + // 标记扫描完成 + ScanManager.markScanComplete(context) + val lastScanTime = ScanManager.getLastScanTime(context) + LogUtil.logDebug( + TAG, "PdfScanner: ✅ 扫描完成,记录时间: ${java.util.Date(lastScanTime)}" + ) + + callback.invoke(true) + } catch (e: Exception) { - Log.e("ocean", "PdfScanner: ❌ 扫描出错: ${e.message}", e) + Log.e(TAG, "PdfScanner: ❌ 扫描出错: ${e.message}", e) callback.invoke(false) } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/PdfViewModel.kt b/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/PdfViewModel.kt new file mode 100644 index 0000000..ce5a48b --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/PdfViewModel.kt @@ -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() + val pdfDocument: LiveData = _pdfDocument + + fun getPDFDocument(filePath: String) { + viewModelScope.launch { + val document = pdfRepository.getDocumentByPath(filePath) + Log.d("ocean", "getPDFDocument->loadPdf: filePath=$filePath, document=$document") + _pdfDocument.postValue(document) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/dr_pdf_scroll_handle.xml b/app/src/main/res/drawable/dr_pdf_scroll_handle.xml new file mode 100644 index 0000000..fe928f4 --- /dev/null +++ b/app/src/main/res/drawable/dr_pdf_scroll_handle.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dr_scroll_handle_bottom.xml b/app/src/main/res/drawable/dr_scroll_handle_bottom.xml new file mode 100644 index 0000000..1e4921e --- /dev/null +++ b/app/src/main/res/drawable/dr_scroll_handle_bottom.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dr_scroll_handle_left.xml b/app/src/main/res/drawable/dr_scroll_handle_left.xml new file mode 100644 index 0000000..f1dfe6b --- /dev/null +++ b/app/src/main/res/drawable/dr_scroll_handle_left.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dr_scroll_handle_right.xml b/app/src/main/res/drawable/dr_scroll_handle_right.xml new file mode 100644 index 0000000..237548d --- /dev/null +++ b/app/src/main/res/drawable/dr_scroll_handle_right.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dr_scroll_handle_top.xml b/app/src/main/res/drawable/dr_scroll_handle_top.xml new file mode 100644 index 0000000..51e3500 --- /dev/null +++ b/app/src/main/res/drawable/dr_scroll_handle_top.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_pdf_view.xml b/app/src/main/res/layout/activity_pdf_view.xml index a56a0dd..4cabd6f 100644 --- a/app/src/main/res/layout/activity_pdf_view.xml +++ b/app/src/main/res/layout/activity_pdf_view.xml @@ -6,6 +6,7 @@ android:orientation="vertical"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_pdf_item.xml b/app/src/main/res/layout/adapter_pdf_item.xml index 8ddc86b..dc2d91a 100644 --- a/app/src/main/res/layout/adapter_pdf_item.xml +++ b/app/src/main/res/layout/adapter_pdf_item.xml @@ -39,7 +39,7 @@ android:id="@+id/tvFileName" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:ellipsize="marquee" + android:ellipsize="middle" android:maxLines="1" android:text="@string/app_name" android:textColor="@color/black" diff --git a/app/src/main/res/layout/dialog_pdf_password.xml b/app/src/main/res/layout/dialog_pdf_password.xml new file mode 100644 index 0000000..b910ae1 --- /dev/null +++ b/app/src/main/res/layout/dialog_pdf_password.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4b76287..c505e03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,4 +24,8 @@ Permission Denied Unable to access PDF file, please grant storage permission in settings File does not exist + Lock + Unlock + Eye Protect + Bookmarks \ No newline at end of file