添加合并pdf,遇到有密码的文件。正常处理。
This commit is contained in:
parent
0bc4a4fab2
commit
8504e7cbea
@ -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
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user