修复打印pdf功能,修复移除密码功能
This commit is contained in:
parent
9f023a0461
commit
56ce37d303
@ -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()
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -71,7 +71,6 @@ class PdfScanner(
|
||||
pdfRepository.insertOrUpdateDocument(updatedDoc)
|
||||
LogUtil.logDebug(TAG, "✅数据库已更新: ${doc.fileName}")
|
||||
|
||||
if (!currentIsPassword) {
|
||||
// 异步生成缩略图,但要避免阻塞
|
||||
launch(Dispatchers.IO) {
|
||||
LogUtil.logDebug(TAG, "异步获取图片更新数据")
|
||||
@ -84,12 +83,6 @@ class PdfScanner(
|
||||
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,7 +139,6 @@ 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) {
|
||||
@ -157,7 +149,6 @@ class PdfScanner(
|
||||
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LogUtil.logDebug(TAG, " 📋 文件已存在数据库: ${file.name}")
|
||||
// 🔹 文件已存在,检查是否需要更新
|
||||
@ -191,8 +182,6 @@ class PdfScanner(
|
||||
LogUtil.logDebug(TAG, "⏩ 无需更新: ${file.name}")
|
||||
}
|
||||
|
||||
// 处理缩略图
|
||||
if (!currentIsPassword) {
|
||||
launch(Dispatchers.IO) {
|
||||
LogUtil.logDebug(TAG, "异步获取图片更新数据")
|
||||
val newThumbnail = generateFastThumbnail(context, file)
|
||||
@ -204,10 +193,6 @@ class PdfScanner(
|
||||
LogUtil.logDebug(TAG, "✅ 缩略图已更新")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val noThumbDoc = updatedDoc.copy(thumbnailPath = null)
|
||||
pdfRepository.insertOrUpdateDocument(noThumbDoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +163,6 @@ class PdfViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fileActionEvent.postValue(
|
||||
FileActionEvent.SetPassword(
|
||||
FileActionEvent.SetPassword.Status.COMPLETE,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user