修复打印pdf功能,修复移除密码功能

This commit is contained in:
ocean 2025-09-12 14:50:48 +08:00
parent 9f023a0461
commit 56ce37d303
8 changed files with 155 additions and 82 deletions

View File

@ -0,0 +1,9 @@
package com.all.pdfreader.pro.app.model
sealed class PrintResult {
object Success : PrintResult()
object PasswordRequired : PrintResult()
object MalformedPdf : PrintResult()
object DeviceNotSupported : PrintResult()
data class Error(val throwable: Throwable) : PrintResult()
}

View File

@ -12,8 +12,23 @@ import android.print.PrintDocumentInfo
import java.io.FileOutputStream
import java.io.InputStream
/**
* 自定义的 PrintDocumentAdapter用于告诉系统如何布局和写入 PDF 内容
*
* @param context 上下文
* @param uri 需要打印的 PDF 文件 Uri
*/
class PrintPdfAdapter(private val context: Context, private val uri: Uri) : PrintDocumentAdapter() {
/**
* 打印布局阶段调用的方法
*
* @param printAttributes 打印属性如纸张大小方向等
* @param printAttributes2 可能更新后的打印属性
* @param cancellationSignal 取消信号可以用来判断用户是否取消了打印
* @param layoutResultCallback 用于通知系统布局结果
* @param bundle 额外参数一般为空
*/
override fun onLayout(
printAttributes: PrintAttributes?,
printAttributes2: PrintAttributes?,
@ -22,15 +37,27 @@ class PrintPdfAdapter(private val context: Context, private val uri: Uri) : Prin
bundle: Bundle?
) {
if (cancellationSignal.isCanceled) {
// 如果用户取消了打印,通知系统取消布局
layoutResultCallback.onLayoutCancelled()
} else {
// 布局完成,告知系统文档信息
layoutResultCallback.onLayoutFinished(
PrintDocumentInfo.Builder("AllPDF")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build(), true
PrintDocumentInfo.Builder("AllPDF") // 设置文档名称
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT) // 设置文档类型
.build(),
true // 表示布局内容有变化
)
}
}
/**
* 实际写入 PDF 数据到打印系统的方法
*
* @param pageRangeArr 需要打印的页码范围
* @param parcelFileDescriptor 系统提供的文件描述符打印内容要写入这里
* @param cancellationSignal 用户取消打印时会触发用于提前终止写入
* @param writeResultCallback 用于通知系统写入结果
*/
override fun onWrite(
pageRangeArr: Array<PageRange?>?,
parcelFileDescriptor: ParcelFileDescriptor,
@ -38,16 +65,24 @@ class PrintPdfAdapter(private val context: Context, private val uri: Uri) : Prin
writeResultCallback: WriteResultCallback
) {
try {
// 打开输入流读取 PDF 文件
val openInputStream: InputStream? = context.contentResolver.openInputStream(uri)
// 打开输出流,将 PDF 内容写入到系统提供的文件描述符
val fileOutputStream = FileOutputStream(parcelFileDescriptor.fileDescriptor)
val bArr = ByteArray(1024)
val bArr = ByteArray(1024) // 1KB 缓冲区
while (true) {
// 循环读取文件内容
val read = openInputStream?.read(bArr)
if (read != null) {
if (read > 0) {
// 写入到输出流
fileOutputStream.write(bArr, 0, read)
} else {
// 读取完成,通知系统写入成功
writeResultCallback.onWriteFinished(arrayOf(PageRange.ALL_PAGES))
// 关闭流,释放资源
openInputStream.close()
fileOutputStream.close()
return
@ -56,6 +91,7 @@ class PrintPdfAdapter(private val context: Context, private val uri: Uri) : Prin
}
} catch (e: Exception) {
e.printStackTrace()
// 如果出错,可以考虑调用 writeResultCallback.onWriteFailed()
}
}
}

View File

@ -9,6 +9,7 @@ import android.widget.Toast
import androidx.fragment.app.activityViewModels
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.DialogListMoreBinding
import com.all.pdfreader.pro.app.model.PrintResult
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.util.AppUtils.dpToPx
import com.all.pdfreader.pro.app.util.AppUtils.printPdfFile
@ -101,7 +102,28 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
dismiss()
}
binding.printBtn.setOnClickListener {
printPdfFile(requireActivity(), Uri.fromFile(File(pdfDocument.filePath)))
val result = printPdfFile(requireActivity(), Uri.fromFile(File(pdfDocument.filePath)))
when (result) {
PrintResult.DeviceNotSupported -> {
Toast.makeText(context, R.string.device_does_not_support_printing, Toast.LENGTH_LONG).show()
}
is PrintResult.Error -> {
Toast.makeText(context, R.string.pdf_cannot_print_error, Toast.LENGTH_LONG).show()
}
PrintResult.MalformedPdf -> {
Toast.makeText(context, R.string.cannot_print_malformed_pdf, Toast.LENGTH_LONG).show()
}
PrintResult.PasswordRequired -> {
Toast.makeText(context, R.string.pdf_cant_print_password_protected, Toast.LENGTH_LONG).show()
}
PrintResult.Success -> {
}
}
dismiss()
}
binding.duplicateFileBtn.setOnClickListener {

View File

@ -4,6 +4,7 @@ import android.graphics.Color
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -16,8 +17,10 @@ import com.all.pdfreader.pro.app.databinding.DialogPdfRemovePasswordBinding
import com.all.pdfreader.pro.app.databinding.DialogPdfSetPasswordBinding
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.util.AppUtils.showKeyboard
import com.all.pdfreader.pro.app.util.FileUtils.isPdfPasswordCorrect
import com.all.pdfreader.pro.app.util.PdfSecurityUtils
import com.all.pdfreader.pro.app.viewmodel.PdfViewModel
import java.io.File
import kotlin.getValue
class PdfRemovePasswordDialog() : DialogFragment(
@ -66,7 +69,7 @@ class PdfRemovePasswordDialog() : DialogFragment(
binding.tvConfirm.setOnClickListener {
val password = binding.etPassword.text.toString()
if (PdfSecurityUtils.isPdfPasswordCorrect(pdfDocument.filePath, password)) {
if (isPdfPasswordCorrect(requireActivity(), File(pdfDocument.filePath), password)) {
viewModel.removePassword(pdfDocument.filePath, password)
dismiss()
} else {

View File

@ -30,10 +30,12 @@ import java.io.File
import java.io.FileOutputStream
import androidx.core.graphics.createBitmap
import androidx.print.PrintHelper
import com.all.pdfreader.pro.app.model.PrintResult
import com.all.pdfreader.pro.app.ui.adapter.PrintPdfAdapter
import com.shockwave.pdfium.PdfDocument
import com.shockwave.pdfium.PdfPasswordException
import com.shockwave.pdfium.PdfiumCore
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.pdmodel.common.PDPageLabelRange
import java.io.IOException
@ -100,7 +102,11 @@ object AppUtils {
fun shareFile(context: Context, file: File) {
try {
if (!file.exists()) {
Toast.makeText(context, context.getString(R.string.error_file_not_exist), Toast.LENGTH_SHORT).show()
Toast.makeText(
context,
context.getString(R.string.error_file_not_exist),
Toast.LENGTH_SHORT
).show()
return
}
@ -136,37 +142,61 @@ object AppUtils {
}
/**
* 打印PDF传入uri
* 打印 PDF 文件的方法
*
* @param context 上下文用于访问系统服务和资源
* @param uri 需要打印的 PDF 文件的 Uri
* @return PrintResult 打印结果的状态成功密码保护设备不支持等
*/
fun printPdfFile(context: Context, uri: Uri?) {
try {
PdfiumCore(context).newDocument(
context.contentResolver.openFileDescriptor(
uri!!,
PDPageLabelRange.STYLE_ROMAN_LOWER
)
fun printPdfFile(context: Context, uri: Uri?): PrintResult {
return try {
// 打开文件描述符,用于访问 PDF 文件内容
val pd = context.contentResolver.openFileDescriptor(
uri!!,
PDPageLabelRange.STYLE_ROMAN_LOWER
)
// 使用 PdfiumCore 尝试打开 PDF如果无法打开会抛出异常
PdfiumCore(context).newDocument(pd)
// val tempFile = File(context.cacheDir, "temp_print.pdf")
// PDDocument.load(context.contentResolver.openInputStream(uri), "").use { document ->
// document.save(tempFile) // 保存解密后的 PDF耗时操作后续看是否做打印需要密码的PDF。
//大概流程为:
//1.先制造 PrintResult.PasswordRequired 的异常
//2.输入密码后进行 PDDocument.load 带入密码加载pdf保存在临时文件
//3.把临时文件的地址传入到PrintPdfAdapter中进行系统的打印
// }
// 检查设备是否支持打印功能
if (PrintHelper.systemSupportsPrint()) {
val printManager =
context.getSystemService(Context.PRINT_SERVICE) as PrintManager
// 获取系统的打印服务
val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
// 设置打印任务名称(显示在打印队列中)
val str = context.getString(R.string.app_name) + " Document"
// 发起打印任务,传入自定义的 PrintDocumentAdapter
printManager.print(
str,
PrintPdfAdapter(context, uri),
null as PrintAttributes?
null as PrintAttributes? // 打印属性为空,使用系统默认配置
)
return
PrintResult.Success
} else {
// 设备不支持打印
PrintResult.DeviceNotSupported
}
Toast.makeText(context, R.string.device_does_not_support_printing, Toast.LENGTH_LONG).show()
} catch (e: PdfPasswordException) {
Toast.makeText(context, R.string.pdf_cant_print_password_protected, Toast.LENGTH_LONG).show()
e.printStackTrace()
} catch (e2: IOException) {
Toast.makeText(context, R.string.cannot_print_malformed_pdf, Toast.LENGTH_LONG).show()
e2.printStackTrace()
} catch (e3: java.lang.Exception) {
Toast.makeText(context, R.string.pdf_cannot_print_error, Toast.LENGTH_LONG).show()
e3.printStackTrace()
// 捕获 PDF 有密码保护的异常
PrintResult.PasswordRequired
} catch (e: IOException) {
// 捕获 PDF 文件损坏或读取失败的异常
PrintResult.MalformedPdf
} catch (e: Exception) {
// 捕获其他未知异常
PrintResult.Error(e)
}
}

View File

@ -71,24 +71,17 @@ class PdfScanner(
pdfRepository.insertOrUpdateDocument(updatedDoc)
LogUtil.logDebug(TAG, "✅数据库已更新: ${doc.fileName}")
if (!currentIsPassword) {
// 异步生成缩略图,但要避免阻塞
launch(Dispatchers.IO) {
LogUtil.logDebug(TAG, "异步获取图片更新数据")
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && doc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
doc.filePath,
newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
// 异步生成缩略图,但要避免阻塞
launch(Dispatchers.IO) {
LogUtil.logDebug(TAG, "异步获取图片更新数据")
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && doc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
doc.filePath,
newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
} else if (doc.thumbnailPath != null) {
val updatedDocWithoutThumb =
updatedDoc.copy(thumbnailPath = null)
pdfRepository.insertOrUpdateDocument(updatedDocWithoutThumb)
LogUtil.logDebug(TAG, "✅图片为Null: ${doc.fileName}")
}
}
} else {
@ -146,16 +139,14 @@ class PdfScanner(
pdfRepository.insertOrUpdateDocument(document)
LogUtil.logDebug(TAG, " ✅ 已保存到数据库: ${file.name}")
if (!isPassword) {//没有密码的情况下才去获取缩略图
launch(Dispatchers.IO) {
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && document.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
document.filePath,
newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
launch(Dispatchers.IO) {
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && document.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
document.filePath,
newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
}
} else {
@ -191,22 +182,16 @@ class PdfScanner(
LogUtil.logDebug(TAG, "⏩ 无需更新: ${file.name}")
}
// 处理缩略图
if (!currentIsPassword) {
launch(Dispatchers.IO) {
LogUtil.logDebug(TAG, "异步获取图片更新数据")
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && existingDoc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
existingDoc.filePath,
newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
launch(Dispatchers.IO) {
LogUtil.logDebug(TAG, "异步获取图片更新数据")
val newThumbnail = generateFastThumbnail(context, file)
if (newThumbnail != null && existingDoc.thumbnailPath != newThumbnail) {
pdfRepository.updateThumbnailPath(
existingDoc.filePath,
newThumbnail
)
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
}
} else {
val noThumbDoc = updatedDoc.copy(thumbnailPath = null)
pdfRepository.insertOrUpdateDocument(noThumbDoc)
}
}
}

View File

@ -42,15 +42,4 @@ object PdfSecurityUtils {
}
}
fun isPdfPasswordCorrect(filePath: String, password: String): Boolean {
return try {
// 只尝试打开 PDF不读取页面内容
PDDocument.load(File(filePath), password).use { document ->
// 如果能成功打开且不抛异常,则密码正确
true
}
} catch (e: Exception) {
false
}
}
}

View File

@ -163,7 +163,6 @@ class PdfViewModel : ViewModel() {
}
}
}
_fileActionEvent.postValue(
FileActionEvent.SetPassword(
FileActionEvent.SetPassword.Status.COMPLETE,