From 56ce37d3033c933cc6ad206d3d2a61d013923030 Mon Sep 17 00:00:00 2001 From: ocean <503259349@qq.com> Date: Fri, 12 Sep 2025 14:50:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=93=E5=8D=B0pdf?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pdfreader/pro/app/model/PrintResult.kt | 9 +++ .../pro/app/ui/adapter/PrintPdfAdapter.kt | 44 ++++++++++- .../app/ui/dialog/ListMoreDialogFragment.kt | 24 +++++- .../app/ui/dialog/PdfRemovePasswordDialog.kt | 5 +- .../all/pdfreader/pro/app/util/AppUtils.kt | 74 +++++++++++++------ .../all/pdfreader/pro/app/util/PdfScanner.kt | 69 +++++++---------- .../pro/app/util/PdfSecurityUtils.kt | 11 --- .../pro/app/viewmodel/PdfViewModel.kt | 1 - 8 files changed, 155 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/com/all/pdfreader/pro/app/model/PrintResult.kt diff --git a/app/src/main/java/com/all/pdfreader/pro/app/model/PrintResult.kt b/app/src/main/java/com/all/pdfreader/pro/app/model/PrintResult.kt new file mode 100644 index 0000000..49f0c27 --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/model/PrintResult.kt @@ -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() +} diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PrintPdfAdapter.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PrintPdfAdapter.kt index b7d085f..a81cb48 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PrintPdfAdapter.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/adapter/PrintPdfAdapter.kt @@ -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?, 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() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ListMoreDialogFragment.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ListMoreDialogFragment.kt index 7d0f1d1..0b2e855 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ListMoreDialogFragment.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ListMoreDialogFragment.kt @@ -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 { diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PdfRemovePasswordDialog.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PdfRemovePasswordDialog.kt index dedd14d..7670f90 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PdfRemovePasswordDialog.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/PdfRemovePasswordDialog.kt @@ -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 { diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt index f9a669d..56afd3e 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt @@ -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) } } 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 fa781d4..e869966 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 @@ -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) } } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfSecurityUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfSecurityUtils.kt index d74b730..9c8b4a6 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfSecurityUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfSecurityUtils.kt @@ -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 - } - } } 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 index 7c3d7e2..d5e6712 100644 --- 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 @@ -163,7 +163,6 @@ class PdfViewModel : ViewModel() { } } } - _fileActionEvent.postValue( FileActionEvent.SetPassword( FileActionEvent.SetPassword.Status.COMPLETE,