添加合并pdf,遇到有密码的文件。正常处理。

This commit is contained in:
ocean 2025-10-16 18:08:58 +08:00
parent 0bc4a4fab2
commit 8504e7cbea
9 changed files with 227 additions and 100 deletions

View File

@ -32,7 +32,8 @@ data class PdfDocumentEntity(
val metadataCreationDate: Long? = null, // PDF创建时间 val metadataCreationDate: Long? = null, // PDF创建时间
val metadataModificationDate: Long? = null, // PDF修改时间 val metadataModificationDate: Long? = null, // PDF修改时间
val password: String? = null,// PDF密码加密存储
val isPassword: Boolean = false,//是否存在密码 val isPassword: Boolean = false,//是否存在密码
var password: String? = null,// PDF密码加密存储
var isSelected: Boolean = false var isSelected: Boolean = false
) : Parcelable ) : Parcelable

View File

@ -1,5 +1,6 @@
package com.all.pdfreader.pro.app.ui.act package com.all.pdfreader.pro.app.ui.act
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@ -7,21 +8,29 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContract
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfMergeBinding import com.all.pdfreader.pro.app.databinding.ActivityPdfMergeBinding
import com.all.pdfreader.pro.app.model.PdfPickerSource import com.all.pdfreader.pro.app.model.PdfPickerSource
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.adapter.PdfAdapter import com.all.pdfreader.pro.app.ui.adapter.PdfAdapter
import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordProtectionDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation
import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener
import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File
class MergePdfActivity : BaseActivity() { class MergePdfActivity : BaseActivity() {
override val TAG: String = "MergePdfActivity" override val TAG: String = "MergePdfActivity"
private lateinit var binding: ActivityPdfMergeBinding private lateinit var binding: ActivityPdfMergeBinding
private lateinit var selectedList: ArrayList<PdfDocumentEntity> private var selectedList: ArrayList<PdfDocumentEntity> = arrayListOf()
private lateinit var adapter: PdfAdapter private lateinit var adapter: PdfAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -31,12 +40,61 @@ class MergePdfActivity : BaseActivity() {
setupBackPressedCallback() setupBackPressedCallback()
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.bg_color).init() .navigationBarColor(R.color.bg_color).init()
selectedList = requireParcelableArrayList(EXTRA_PDF_LIST) val list: ArrayList<PdfDocumentEntity> = requireParcelableArrayList(EXTRA_PDF_LIST)
updateContinueNowBtnState(selectedList.size >= 2) updateContinueNowBtnState(list.size >= 2)
initView() lifecycleScope.launch {
setupClick() handlePasswordProtectedPdfs(list) {
list.forEach {
logDebug("onCreate password->${it.password}")
logDebug("onCreate isPassword->${it.isPassword}")
}
selectedList.clear()
selectedList.addAll(list)
initView()
setupClick()
}
}
} }
/**
* 遍历集合弹出密码对话框并更新 password 字段
*/
private suspend fun handlePasswordProtectedPdfs(
pdfList: ArrayList<PdfDocumentEntity>,
onAllPasswordsReady: (ArrayList<PdfDocumentEntity>) -> Unit
) {
for (pdf in pdfList) {
if (pdf.isPassword && pdf.password.isNullOrEmpty()) {
val pwd = showPasswordDialogSuspend(File(pdf.filePath)) ?: run {
finish()
return
}
pdf.password = pwd
}
}
onAllPasswordsReady(pdfList)
}
/**
* DialogFragment 封装成 suspend 函数
*/
private suspend fun showPasswordDialogSuspend(file: File): String? =
suspendCancellableCoroutine { cont ->
PdfPasswordProtectionDialogFragment(
file,
onOkClick = { password ->
if (cont.isActive) {
cont.resumeWith(Result.success(password))
}
},
onCancelClick = {
if (cont.isActive) {
cont.resumeWith(Result.success(null))
}
}, isPrompt = true
).show(supportFragmentManager, TAG)
}
private fun initView() { private fun initView() {
adapter = PdfAdapter( adapter = PdfAdapter(
pdfList = selectedList, pdfList = selectedList,
@ -61,9 +119,8 @@ class MergePdfActivity : BaseActivity() {
openPicker() openPicker()
} }
binding.continueNowBtn.setOnSingleClickListener { binding.continueNowBtn.setOnSingleClickListener {
val list = selectedList.map { it.filePath } val intent = PdfResultActivity.createIntentMergePdfActivityToResult(
val intent = PdfResultActivity.createIntentInputFile( this, selectedList, PdfPickerSource.MERGE
this, ArrayList(list), PdfPickerSource.MERGE
) )
startActivity(intent) startActivity(intent)
finish() finish()
@ -73,7 +130,7 @@ class MergePdfActivity : BaseActivity() {
private val pickPdfLauncher = private val pickPdfLauncher =
registerForActivityResult(PickPdfContract(Companion.TAG)) { list -> registerForActivityResult(PickPdfContract(Companion.TAG)) { list ->
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
handleSelectedPdfs(list) handleSelectedPdfs(list as ArrayList<PdfDocumentEntity>)
} }
} }
@ -81,11 +138,20 @@ class MergePdfActivity : BaseActivity() {
pickPdfLauncher.launch(PdfPickerSource.MERGE to selectedList) pickPdfLauncher.launch(PdfPickerSource.MERGE to selectedList)
} }
private fun handleSelectedPdfs(list: List<PdfDocumentEntity>) { @SuppressLint("NotifyDataSetChanged")
selectedList.clear() private fun handleSelectedPdfs(list: ArrayList<PdfDocumentEntity>) {
selectedList.addAll(list) lifecycleScope.launch {
adapter.notifyDataSetChanged() handlePasswordProtectedPdfs(list) {
updateContinueNowBtnState(selectedList.size >= 2) list.forEach {
logDebug("handleSelectedPdfs password->${it.password}")
logDebug("handleSelectedPdfs isPassword->${it.isPassword}")
}
selectedList.clear()
selectedList.addAll(list)
adapter.notifyDataSetChanged()
updateContinueNowBtnState(selectedList.size >= 2)
}
}
} }
private fun updateContinueNowBtnState(b: Boolean) { private fun updateContinueNowBtnState(b: Boolean) {

View File

@ -155,9 +155,16 @@ class PdfPickerActivity : BaseActivity() {
private fun markSelectedItems( private fun markSelectedItems(
sortedList: List<PdfDocumentEntity>, historyList: List<PdfDocumentEntity> sortedList: List<PdfDocumentEntity>, historyList: List<PdfDocumentEntity>
) { ) {
val historySet = historyList.map { it.filePath }.toSet() val historyMap = historyList.associateBy { it.filePath }
sortedList.forEach { item -> sortedList.forEach { item ->
item.isSelected = item.filePath in historySet val historyItem = historyMap[item.filePath]
if (historyItem != null) {
item.isSelected = true
// 同步历史密码
item.password = historyItem.password
} else {
item.isSelected = false
}
} }
} }

View File

@ -16,6 +16,7 @@ import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding
import com.all.pdfreader.pro.app.model.PdfPickerSource import com.all.pdfreader.pro.app.model.PdfPickerSource
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.PdfSplitResultItem import com.all.pdfreader.pro.app.model.PdfSplitResultItem
import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity
import com.all.pdfreader.pro.app.ui.adapter.PdfResultAdapter import com.all.pdfreader.pro.app.ui.adapter.PdfResultAdapter
import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils import com.all.pdfreader.pro.app.util.AppUtils
@ -37,12 +38,13 @@ class PdfResultActivity : BaseActivity() {
companion object { companion object {
private const val EXTRA_SELECTED_LIST = "extra_selected_list" private const val EXTRA_SELECTED_LIST = "extra_selected_list"
private const val EXTRA_FILE_LIST = "extra_file_list" private const val EXTRA_FILE_LIST = "extra_file_list"
private const val EXTRA_MERGE_LIST = "extra_merge_list"//合并传输使用的key
private const val EXTRA_SOURCE = "extra_source" private const val EXTRA_SOURCE = "extra_source"
private const val EXTRA_LOCK_UNLOCK_PASSWORD = "extra_lock_unlock_password" private const val EXTRA_LOCK_UNLOCK_PASSWORD = "extra_lock_unlock_password"
private const val EXTRA_FILE_PATH = "extra_file_path" private const val EXTRA_FILE_PATH = "extra_file_path"
private const val EXTRA_SPLIT_PASSWORD = "extra_split_password" private const val EXTRA_SPLIT_PASSWORD = "extra_split_password"
fun createIntentPdfSelectedPagesItem( fun createIntentSplitPdfActivityToResult(
context: Context, context: Context,
list: ArrayList<PdfSelectedPagesItem>, list: ArrayList<PdfSelectedPagesItem>,
source: PdfPickerSource, source: PdfPickerSource,
@ -64,6 +66,15 @@ class PdfResultActivity : BaseActivity() {
} }
} }
fun createIntentMergePdfActivityToResult(
context: Context, list: ArrayList<PdfDocumentEntity>, source: PdfPickerSource
): Intent {
return Intent(context, PdfResultActivity::class.java).apply {
putParcelableArrayListExtra(EXTRA_MERGE_LIST, list)
putExtra(EXTRA_SOURCE, source)
}
}
fun createIntentLock( fun createIntentLock(
context: Context, filepath: String, password: String, source: PdfPickerSource context: Context, filepath: String, password: String, source: PdfPickerSource
): Intent { ): Intent {
@ -78,15 +89,11 @@ class PdfResultActivity : BaseActivity() {
private lateinit var binding: ActivityPdfSplitResultBinding private lateinit var binding: ActivityPdfSplitResultBinding
private lateinit var adapter: PdfResultAdapter private lateinit var adapter: PdfResultAdapter
private var resultList: MutableList<PdfSplitResultItem> = mutableListOf() private var resultList: MutableList<PdfSplitResultItem> = mutableListOf()
private lateinit var selectedList: ArrayList<PdfSelectedPagesItem>
private lateinit var inputFile: ArrayList<String>
private lateinit var source: PdfPickerSource private lateinit var source: PdfPickerSource
private var isProcessing = false private var isProcessing = false
private var exitDialog: PromptDialogFragment? = null private var exitDialog: PromptDialogFragment? = null
private val pdfRepository = getRepository() private val pdfRepository = getRepository()
private lateinit var pdfScanner: PdfScanner private lateinit var pdfScanner: PdfScanner
private lateinit var filepath: String
private lateinit var lockAndUnlockPassword: String
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -95,35 +102,11 @@ class PdfResultActivity : BaseActivity() {
setupBackPressedCallback() setupBackPressedCallback()
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true) ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.bg_color).init() .navigationBarColor(R.color.bg_color).init()
selectedList = requireParcelableArrayList(EXTRA_SELECTED_LIST)
inputFile = requireStringArrayList(EXTRA_FILE_LIST)
filepath = intent.getStringExtra(EXTRA_FILE_PATH) ?: ""
lockAndUnlockPassword = intent.getStringExtra(EXTRA_LOCK_UNLOCK_PASSWORD) ?: ""
source = getSerializableOrDefault(EXTRA_SOURCE, PdfPickerSource.NONE) source = getSerializableOrDefault(EXTRA_SOURCE, PdfPickerSource.NONE)
if (source == PdfPickerSource.NONE) { if (source == PdfPickerSource.NONE) {
showToast(getString(R.string.pdf_loading_failed)) showToast(getString(R.string.pdf_loading_failed))
finish() finish()
return return
} else {
if (source == PdfPickerSource.SPLIT) {
if (selectedList.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
return
}
} else if (source == PdfPickerSource.MERGE || source == PdfPickerSource.TO_IMAGES) {
if (inputFile.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
return
}
} else if (source == PdfPickerSource.LOCK || source == PdfPickerSource.UNLOCK) {
if (filepath.isEmpty() || lockAndUnlockPassword.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
return
}
}
} }
pdfScanner = PdfScanner(this, pdfRepository) pdfScanner = PdfScanner(this, pdfRepository)
initView() initView()
@ -148,20 +131,26 @@ class PdfResultActivity : BaseActivity() {
binding.progressBar.isIndeterminate = false binding.progressBar.isIndeterminate = false
binding.progressBar.progress = 0 binding.progressBar.progress = 0
binding.progressBar.max = 100 binding.progressBar.max = 100
val selectedList: ArrayList<PdfSelectedPagesItem> =
requireParcelableArrayList(EXTRA_SELECTED_LIST)
if (selectedList.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
return@launch
}
val totalPages = selectedList.sumOf { it.pages.count { it.isSelected } } val totalPages = selectedList.sumOf { it.pages.count { it.isSelected } }
var processedPages = 0 var processedPages = 0
for (item in selectedList) { for (item in selectedList) {
val selectedPages = item.pages.filter { it.isSelected } val selectedPages = item.pages.filter { it.isSelected }
if (selectedPages.isEmpty()) continue if (selectedPages.isEmpty()) continue
val inputFile = File(item.filePath) val splitInputFile = File(item.filePath)
val outputDir = File( val outputDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
"PDFReaderPro/split" "PDFReaderPro/split"
).apply { if (!exists()) mkdirs() } ).apply { if (!exists()) mkdirs() }
PdfUtils.exportSelectedPages( PdfUtils.exportSelectedPages(
inputFile = inputFile, inputFile = splitInputFile,
selectedPages = selectedPages, selectedPages = selectedPages,
outputDir = outputDir, outputDir = outputDir,
outputFileName = "${item.fileName}.pdf", outputFileName = "${item.fileName}.pdf",
@ -172,7 +161,8 @@ class PdfResultActivity : BaseActivity() {
binding.progressTv.text = "$percent" binding.progressTv.text = "$percent"
binding.progressBar.progress = percent binding.progressBar.progress = percent
} }
}, splitPassword },
splitPassword
)?.let { resultFile -> )?.let { resultFile ->
val thumbnails = generateFastThumbnail(this@PdfResultActivity, resultFile) val thumbnails = generateFastThumbnail(this@PdfResultActivity, resultFile)
val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false) val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false)
@ -185,37 +175,51 @@ class PdfResultActivity : BaseActivity() {
binding.progressBar.isIndeterminate = false binding.progressBar.isIndeterminate = false
binding.progressBar.progress = 0 binding.progressBar.progress = 0
binding.progressBar.max = 100 binding.progressBar.max = 100
if (inputFile.isNotEmpty()) { val mergeInputFile: ArrayList<PdfDocumentEntity> =
val inputFiles: List<File> = inputFile.map { path -> File(path) } requireParcelableArrayList(EXTRA_MERGE_LIST)
val outputDir = File( if (mergeInputFile.isEmpty()) {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), showToast(getString(R.string.pdf_loading_failed))
"PDFReaderPro/merge" finish()
).apply { if (!exists()) mkdirs() } return@launch
val outputFileName = }
getString(R.string.merge) + "_" + System.currentTimeMillis() val inputWithPasswords = mergeInputFile.map { pdf ->
.toUnderscoreDateTime() + ".pdf" File(pdf.filePath) to pdf.password
PdfUtils.mergePdfFilesSafe( }
inputFiles = inputFiles, val outputDir = File(
outputDir = outputDir, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
outputFileName = outputFileName, "PDFReaderPro/merge"
onProgress = { current, total -> ).apply { if (!exists()) mkdirs() }
val progressPercent = current * 100 / total val outputFileName =
lifecycleScope.launch(Dispatchers.Main) { getString(R.string.merge) + "_" + System.currentTimeMillis()
binding.progressBar.progress = progressPercent .toUnderscoreDateTime() + ".pdf"
binding.progressTv.text = "$progressPercent" PdfUtils.mergePdfFilesWithPassword(
} inputFiles = inputWithPasswords,
})?.let { resultFile -> outputDir = outputDir,
val thumbnails = generateFastThumbnail(this@PdfResultActivity, resultFile) outputFileName = outputFileName,
val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false) onProgress = { current, total ->
pdfScanner.addNewPdfToDatabase(result.filePath, result.thumbnailPath) { val progressPercent = current * 100 / total
resultList.add(result) lifecycleScope.launch(Dispatchers.Main) {
binding.progressBar.progress = progressPercent
binding.progressTv.text = "$progressPercent"
} }
})?.let { resultFile ->
val thumbnails = generateFastThumbnail(this@PdfResultActivity, resultFile)
val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false)
pdfScanner.addNewPdfToDatabase(result.filePath, result.thumbnailPath) {
resultList.add(result)
} }
} }
} else if (source == PdfPickerSource.LOCK) { } else if (source == PdfPickerSource.LOCK) {
val filepath = intent.getStringExtra(EXTRA_FILE_PATH) ?: ""
if (filepath.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
return@launch
}
val lockPassword = intent.getStringExtra(EXTRA_LOCK_UNLOCK_PASSWORD) ?: ""
binding.congratulationsDesc.text = getString(R.string.set_password_successfully) binding.congratulationsDesc.text = getString(R.string.set_password_successfully)
PdfSecurityUtils.setPasswordToPdfWithProgress( PdfSecurityUtils.setPasswordToPdfWithProgress(
filepath, lockAndUnlockPassword, lockAndUnlockPassword filepath, lockPassword, lockPassword
) { progress -> ) { progress ->
binding.progressTv.text = "$progress" binding.progressTv.text = "$progress"
}.let { it -> }.let { it ->
@ -226,26 +230,38 @@ class PdfResultActivity : BaseActivity() {
resultList.add(result) resultList.add(result)
} }
} else if (source == PdfPickerSource.UNLOCK) { } else if (source == PdfPickerSource.UNLOCK) {
val filepath = intent.getStringExtra(EXTRA_FILE_PATH) ?: ""
if (filepath.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
return@launch
}
val unlockPassword = intent.getStringExtra(EXTRA_LOCK_UNLOCK_PASSWORD) ?: ""
binding.congratulationsDesc.text = getString(R.string.remove_password_successfully) binding.congratulationsDesc.text = getString(R.string.remove_password_successfully)
PdfSecurityUtils.removePasswordFromPdfWithProgress(filepath, lockAndUnlockPassword) { progress -> PdfSecurityUtils.removePasswordFromPdfWithProgress(
filepath, unlockPassword
) { progress ->
binding.progressTv.text = "$progress" binding.progressTv.text = "$progress"
}?.let { it -> }?.let { it ->
val result = PdfSplitResultItem( val result = PdfSplitResultItem(
filePath = filepath, filePath = filepath, thumbnailPath = it, isPassword = false
thumbnailPath = it,
isPassword = false
) )
resultList.add(result) resultList.add(result)
} }
} else if (source == PdfPickerSource.TO_IMAGES) { } else if (source == PdfPickerSource.TO_IMAGES) {
val inputFiles: List<File> = inputFile.map { path -> File(path) } val files = requireStringArrayList(EXTRA_FILE_LIST)
if (files.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
return@launch
}
val inputFiles: List<File> = files.map { path -> File(path) }
val outputDir = File( val outputDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
"PDFReaderPro/img2Pdf" "PDFReaderPro/img2Pdf"
).apply { if (!exists()) mkdirs() } ).apply { if (!exists()) mkdirs() }
val outputFileName = val outputFileName = getString(R.string.app_name) + "_" + System.currentTimeMillis()
getString(R.string.app_name) + "_" + System.currentTimeMillis() .toUnderscoreDateTime() + ".pdf"
.toUnderscoreDateTime() + ".pdf"
PdfUtils.imgToPdfFilesSafe( PdfUtils.imgToPdfFilesSafe(
inputFiles = inputFiles, inputFiles = inputFiles,
outputDir = outputDir, outputDir = outputDir,

View File

@ -141,7 +141,7 @@ class SplitPdfActivity : BaseActivity() {
binding.splitBtn.setOnSingleClickListener { binding.splitBtn.setOnSingleClickListener {
logDebug("${selectedList.size}") logDebug("${selectedList.size}")
//因为图片做的路径缓存方式所以这里直接传入整个集合到result页处理 //因为图片做的路径缓存方式所以这里直接传入整个集合到result页处理
val intent = PdfResultActivity.createIntentPdfSelectedPagesItem( val intent = PdfResultActivity.createIntentSplitPdfActivityToResult(
this, this,
ArrayList(selectedList), ArrayList(selectedList),
PdfPickerSource.SPLIT, PdfPickerSource.SPLIT,

View File

@ -16,7 +16,8 @@ import java.io.File
class PdfPasswordProtectionDialogFragment( class PdfPasswordProtectionDialogFragment(
private val file: File, private val file: File,
private val onOkClick: (String) -> Unit, private val onOkClick: (String) -> Unit,
private val onCancelClick: () -> Unit private val onCancelClick: () -> Unit,
private val isPrompt: Boolean = false
) : DialogFragment() { ) : DialogFragment() {
private lateinit var binding: DialogPdfPasswordProtectionBinding private lateinit var binding: DialogPdfPasswordProtectionBinding
@ -47,6 +48,12 @@ class PdfPasswordProtectionDialogFragment(
binding.etPassword.showKeyboard() binding.etPassword.showKeyboard()
if (isPrompt) {
binding.promptTv.visibility = View.VISIBLE
binding.promptTv.text = getString(R.string.file_is_password_protected, file.name)
} else {
binding.promptTv.visibility = View.GONE
}
binding.tvCancel.setOnClickListener { binding.tvCancel.setOnClickListener {
onCancelClick() onCancelClick()

View File

@ -193,8 +193,8 @@ object PdfUtils {
* @param onProgress 可选回调当前处理进度 (current 文件, total 文件) * @param onProgress 可选回调当前处理进度 (current 文件, total 文件)
* @return 新生成的 PDF 文件失败返回 null * @return 新生成的 PDF 文件失败返回 null
*/ */
suspend fun mergePdfFilesSafe( suspend fun mergePdfFilesWithPassword(
inputFiles: List<File>, inputFiles: List<Pair<File, String?>>, // Pair<文件, 密码>
outputDir: File, outputDir: File,
outputFileName: String, outputFileName: String,
onProgress: ((current: Int, total: Int) -> Unit)? = null onProgress: ((current: Int, total: Int) -> Unit)? = null
@ -209,9 +209,26 @@ object PdfUtils {
val merger = PDFMergerUtility() val merger = PDFMergerUtility()
val total = 100 val total = 100
// 加载文件 0~30% // 0~30%: 加载 PDF
inputFiles.forEachIndexed { index, file -> inputFiles.forEachIndexed { index, (file, password) ->
merger.addSource(file) val doc = if (!password.isNullOrEmpty()) {
PDDocument.load(file, password)
} else {
PDDocument.load(file)
}
// 移除加密保护
if (doc.isEncrypted) {
doc.isAllSecurityToBeRemoved = true
}
// 将文档临时保存到内存文件,然后添加给 merger
val tempFile = File.createTempFile("merge_", ".pdf")
doc.save(tempFile)
doc.close()
merger.addSource(tempFile)
val progress = ((index + 1).toFloat() / inputFiles.size * 30).toInt() val progress = ((index + 1).toFloat() / inputFiles.size * 30).toInt()
onProgress?.invoke(progress, total) onProgress?.invoke(progress, total)
delay(50) delay(50)
@ -219,17 +236,18 @@ object PdfUtils {
merger.destinationFileName = outputFile.absolutePath merger.destinationFileName = outputFile.absolutePath
// 伪进度30%~95% // 伪进度 30~95%
var fakeProgress = 30 var fakeProgress = 30
val fakeJob = launch { val fakeJob = launch {
while (fakeProgress < 95) { while (fakeProgress < 95) {
fakeProgress += (1..3).random() // 每次前进一点点 fakeProgress += (1..3).random()
if (fakeProgress > 95) fakeProgress = 95 if (fakeProgress > 95) fakeProgress = 95
onProgress?.invoke(fakeProgress, total) onProgress?.invoke(fakeProgress, total)
delay((100L..300L).random()) // 模拟不均匀的进度 delay((100L..300L).random())
} }
} }
// 真正合并
merger.mergeDocuments(MemoryUsageSetting.setupTempFileOnly()) merger.mergeDocuments(MemoryUsageSetting.setupTempFileOnly())
fakeJob.cancel() fakeJob.cancel()

View File

@ -9,20 +9,31 @@
<TextView <TextView
android:id="@+id/tvTitle" android:id="@+id/tvTitle"
style="@style/TextViewFont_PopSemiBold"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
style="@style/TextViewFont_PopSemiBold"
android:text="@string/password_protection" android:text="@string/password_protection"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="20sp" /> android:textSize="20sp" />
<TextView <TextView
android:id="@+id/tvMessage" android:id="@+id/promptTv"
style="@style/TextViewFont_PopMedium"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:text="@string/file_is_password_protected"
android:textColor="@color/black"
android:textSize="14sp"
android:visibility="gone" />
<TextView
android:id="@+id/tvMessage"
style="@style/TextViewFont_PopRegular" style="@style/TextViewFont_PopRegular"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/password_protection_dialog_desc" android:text="@string/password_protection_dialog_desc"
android:textColor="@color/black_80" android:textColor="@color/black_80"
android:textSize="14sp" /> android:textSize="14sp" />
@ -37,14 +48,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/enter_password" android:hint="@string/enter_password"
app:endIconMode="password_toggle" app:endIconDrawable="@drawable/dr_password_state"
app:endIconDrawable="@drawable/dr_password_state"> app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword" android:id="@+id/etPassword"
style="@style/TextViewFont_PopRegular"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/TextViewFont_PopRegular"
android:inputType="textPassword" android:inputType="textPassword"
android:textSize="16sp" /> android:textSize="16sp" />
@ -61,19 +72,19 @@
<TextView <TextView
android:id="@+id/tvCancel" android:id="@+id/tvCancel"
style="@style/TextViewFont_PopRegular"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
style="@style/TextViewFont_PopRegular"
android:padding="12dp" android:padding="12dp"
android:text="@string/cancel" android:text="@string/cancel"
android:textColor="@color/black_80" /> android:textColor="@color/black_80" />
<TextView <TextView
android:id="@+id/tvConfirm" android:id="@+id/tvConfirm"
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/TextViewFont_PopMedium"
android:padding="12dp" android:padding="12dp"
android:text="@string/ok" android:text="@string/ok"
android:textColor="@color/black" /> android:textColor="@color/black" />

View File

@ -21,6 +21,7 @@
<string name="created_date">Created Date</string> <string name="created_date">Created Date</string>
<string name="path">Path</string> <string name="path">Path</string>
<string name="path_details">Path: %1$s</string> <string name="path_details">Path: %1$s</string>
<string name="file_is_password_protected">%1$s is password protected</string>
<string name="file_name">File Name</string> <string name="file_name">File Name</string>
<string name="file_size">File Size</string> <string name="file_size">File Size</string>
<string name="ascending">Ascending</string> <string name="ascending">Ascending</string>