diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3ff13d9..5e7cb52 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,4 +63,7 @@ dependencies { implementation(libs.pdfbox.android) implementation(libs.jp2forandroid) implementation(libs.flexbox) + implementation(libs.pictureselector) + implementation(libs.ucrop) + implementation(libs.compress) } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c639003..e43e75c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,4 +20,9 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class com.shockwave.** \ No newline at end of file +-keep class com.shockwave.** +-keep class com.luck.picture.lib.** { *; } +-keep class com.luck.lib.camerax.** { *; } +-dontwarn com.yalantis.ucrop** +-keep class com.yalantis.ucrop** { *; } +-keep interface com.yalantis.ucrop** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c79ea4..bd1d9e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,8 +4,15 @@ android:theme="@style/Theme.PDFReaderPro" tools:ignore="DiscouragedApi,LockedOrientationActivity,RedundantLabel"> - + + + + + + @@ -107,5 +114,16 @@ android:resource="@xml/file_paths" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt index ff7963f..6ddcec6 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt @@ -10,6 +10,7 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import com.all.pdfreader.pro.app.PRApp import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding import com.all.pdfreader.pro.app.model.PdfPickerSource @@ -105,7 +106,7 @@ class PdfResultActivity : BaseActivity() { finish() return } - } else if (source == PdfPickerSource.MERGE) { + } else if (source == PdfPickerSource.MERGE || source == PdfPickerSource.TO_IMAGES) { if (inputFile.isEmpty()) { showToast(getString(R.string.pdf_loading_failed)) finish() @@ -229,6 +230,34 @@ class PdfResultActivity : BaseActivity() { ) resultList.add(result) } + } else if(source == PdfPickerSource.TO_IMAGES){ + val inputFiles: List = inputFile.map { path -> File(path) } + val outputDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), + "PDFReaderPro/img2Pdf" + ).apply { if (!exists()) mkdirs() } + val outputFileName = + getString(R.string.app_name) + "_" + System.currentTimeMillis() + .toUnderscoreDateTime() + ".pdf" + PdfUtils.imgToPdfFilesSafe( + inputFiles = inputFiles, + outputDir = outputDir, + outputFileName = outputFileName, + onProgress = { current, total -> + logDebug("current->$current total->$total") + val progressPercent = current * 100 / total + logDebug("progressPercent->$progressPercent") + runOnUiThread { + 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) + } + } } withContext(Dispatchers.Main) { binding.processingLayout.visibility = View.GONE diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt index b60fee6..08b0f72 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt @@ -16,9 +16,11 @@ import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem import com.all.pdfreader.pro.app.model.RenameType import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter import com.all.pdfreader.pro.app.ui.adapter.SplitSelectedPdfAdapter +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.RenameDialogFragment import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener +import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted import com.all.pdfreader.pro.app.util.FileUtils.toUnderscoreDateTime import com.all.pdfreader.pro.app.util.PdfUtils import com.gyf.immersionbar.ImmersionBar @@ -149,6 +151,24 @@ class SplitPdfActivity : BaseActivity() { } private fun initSplitData(file: File) { + lifecycleScope.launch { + // 是否存在密码 + val isEncrypted = withContext(Dispatchers.IO) { + isPdfEncrypted(file) + } + if (isEncrypted) { + PdfPasswordProtectionDialogFragment(file, onOkClick = { password -> + initSplitDataWithPassword(file, password) + }, onCancelClick = { + finish() + }).show(supportFragmentManager, TAG) + } else { + initSplitDataWithPassword(file) + } + } + } + + private fun initSplitDataWithPassword(file: File, password: String? = null) { lifecycleScope.launch { splitList.clear() // 先切换到 IO 线程执行删除,等待完成 @@ -157,7 +177,11 @@ class SplitPdfActivity : BaseActivity() { } // 删除完成后,开始收集数据 var firstPageLoaded = false - PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem -> + PdfUtils.splitPdfToPageItemsFlow( + context = this@SplitPdfActivity, + inputFile = file, + password = password + ).collect { pageItem -> logDebug("splitPdfToPageItemsFlow pageItem->$pageItem") if (splitList.size <= pageItem.pageIndex) { splitList.add(pageItem) diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt index e8b57a8..2cd2a42 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding import com.all.pdfreader.pro.app.room.repository.PdfRepository import com.all.pdfreader.pro.app.sp.AppStore +import com.all.pdfreader.pro.app.util.ToastUtils abstract class BaseFrag : Fragment() { protected abstract val TAG: String @@ -59,6 +60,10 @@ abstract class BaseFrag : Fragment() { Log.w("ocean", "$TAG: $message") } + protected fun showToast(message: String) { + ToastUtils.show(requireActivity(), message) + } + //获取数据库实例 protected fun getRepository(): PdfRepository { return PdfRepository.getInstance() diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt index 36bb404..0eaf49b 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt @@ -1,15 +1,39 @@ package com.all.pdfreader.pro.app.ui.fragment +import android.content.Context +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.net.toUri import androidx.fragment.app.Fragment +import com.all.pdfreader.pro.app.PRApp +import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.databinding.FragmentToolsBinding import com.all.pdfreader.pro.app.model.PdfPickerSource import com.all.pdfreader.pro.app.ui.act.PdfPickerActivity +import com.all.pdfreader.pro.app.ui.act.PdfResultActivity +import com.all.pdfreader.pro.app.util.GlideEngine +import com.luck.picture.lib.basic.PictureSelector +import com.luck.picture.lib.config.PictureMimeType +import com.luck.picture.lib.config.SelectMimeType +import com.luck.picture.lib.config.SelectModeConfig +import com.luck.picture.lib.engine.CompressFileEngine +import com.luck.picture.lib.entity.LocalMedia +import com.luck.picture.lib.interfaces.OnKeyValueResultCallbackListener +import com.luck.picture.lib.interfaces.OnMediaEditInterceptListener +import com.luck.picture.lib.interfaces.OnResultCallbackListener +import com.luck.picture.lib.style.PictureSelectorStyle +import com.luck.picture.lib.style.SelectMainStyle +import com.luck.picture.lib.utils.DateUtils +import com.yalantis.ucrop.UCrop +import top.zibin.luban.Luban +import top.zibin.luban.OnNewCompressListener +import java.io.File -class ToolsFrag : Fragment() { +class ToolsFrag : BaseFrag() { + override val TAG: String = "ToolsFrag" private lateinit var binding: FragmentToolsBinding override fun onCreateView( @@ -41,5 +65,110 @@ class ToolsFrag : Fragment() { val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.UNLOCK) startActivity(intent) } + binding.imgToPdfBtn.setOnClickListener { + openImagePicker() + } + } + + private fun openImagePicker() { + val selectorStyle = PictureSelectorStyle().apply { + selectMainStyle = SelectMainStyle().apply { + isSelectNumberStyle = true + isPreviewSelectNumberStyle = true + selectBackground = R.drawable.num_checkbox_selector + } + } + PictureSelector.create(this) + .openGallery(SelectMimeType.ofImage()) + .setSelectorUIStyle(selectorStyle) + .setImageEngine(GlideEngine.createGlideEngine()) + .setImageSpanCount(3) + .isGif(false) + .isFastSlidingSelect(true) + .setSelectionMode(SelectModeConfig.MULTIPLE) + .setMaxSelectNum(50) + + .setCompressEngine(object : CompressFileEngine { + override fun onStartCompress( + context: Context?, + source: ArrayList?, + call: OnKeyValueResultCallbackListener? + ) { + Luban.with(context) + .load(source) + .ignoreBy(100) + .setCompressListener(object : OnNewCompressListener { + override fun onStart() { + + } + + override fun onSuccess( + source: String?, + compressFile: File? + ) { + call?.onCallback(source, compressFile?.absolutePath) + } + + override fun onError(source: String?, e: Throwable?) { + call?.onCallback(source, null) + } + }) + .launch() + } + }) + .setEditMediaInterceptListener(object : OnMediaEditInterceptListener { + override fun onStartMediaEdit( + fragment: Fragment, currentLocalMedia: LocalMedia, requestCode: Int + ) { + val currentEditPath = currentLocalMedia.getAvailablePath() + val inputUri = if (PictureMimeType.isContent(currentEditPath)) { + currentEditPath.toUri() + } else { + Uri.fromFile(File(currentEditPath)) + } + val destinationUri = Uri.fromFile( + File( + getSandboxPath(), DateUtils.getCreateFileName("CROP_") + ".jpeg" + ) + ) + val uCrop = UCrop.of(inputUri, destinationUri) + val buildOptions = UCrop.Options().apply { + isDarkStatusBarBlack(true) + } + uCrop.withOptions(buildOptions) + uCrop.start(requireActivity(), fragment, requestCode) + } + + }).forResult(object : OnResultCallbackListener { + override fun onResult(result: ArrayList) { + logDebug("forResult ->${result.size}") + if (result.isNotEmpty()) { + // result 中可能有带裁剪的 Uri 或原图 Uri + // 取出路径列表 + val paths = result.mapNotNull { media -> + media.availablePath + } + if (paths.isEmpty()) { + return + } + val intent = PdfResultActivity.createIntentInputFile( + requireActivity(), + ArrayList(paths), + PdfPickerSource.TO_IMAGES + ) + startActivity(intent) + } + } + + override fun onCancel() { + logDebug("onCancel") + } + }) + } + + fun getSandboxPath(): String { + val dir = File(PRApp.getContext().externalCacheDir, "SandboxPdfPro") + if (!dir.exists()) dir.mkdirs() + return dir.absolutePath } } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt index e2434b1..b72eb6c 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt @@ -6,7 +6,7 @@ import android.graphics.pdf.PdfRenderer import android.net.Uri import android.os.ParcelFileDescriptor import android.provider.MediaStore -import android.provider.OpenableColumns +import android.provider.OpenableColumns import android.util.Log import com.all.pdfreader.pro.app.PRApp import com.all.pdfreader.pro.app.R @@ -21,6 +21,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import com.all.pdfreader.pro.app.model.RenameResult +import androidx.core.net.toUri object FileUtils { @@ -330,6 +331,7 @@ object FileUtils { val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH) return sdf.format(Date(this)) } + data class FileInfo( val name: String, val size: Long, val uri: Uri ) @@ -368,11 +370,12 @@ object FileUtils { */ fun isPdfPasswordCorrect(context: Context, file: File, password: String): Boolean { return try { - val descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) - val pdfiumCore = PdfiumCore(context) - val doc = pdfiumCore.newDocument(descriptor, password) - pdfiumCore.closeDocument(doc) - true // 没抛异常说明密码正确 + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { descriptor -> + val pdfiumCore = PdfiumCore(context) + val doc = pdfiumCore.newDocument(descriptor, password) + pdfiumCore.closeDocument(doc) + } + true// 没抛异常说明密码正确 } catch (e: Exception) { false // 抛异常说明密码错误 } @@ -557,4 +560,28 @@ object FileUtils { return newFile } + + fun uriToFile(context: Context, path: String): File? { + return try { + if (path.startsWith("content://")) { + // 是 content:// Uri,需要拷贝到临时文件 + val uri = path.toUri() + val inputStream = context.contentResolver.openInputStream(uri) ?: return null + val tempFile = File(context.cacheDir, "temp_${System.currentTimeMillis()}.jpg") + tempFile.outputStream().use { output -> + inputStream.copyTo(output) + } + inputStream.close() + tempFile + } else { + // 是普通文件路径 + val file = File(path) + if (file.exists()) file else null + } + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/GlideEngine.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/GlideEngine.kt new file mode 100644 index 0000000..5ecc988 --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/GlideEngine.kt @@ -0,0 +1,87 @@ +package com.all.pdfreader.pro.app.util + +import android.content.Context +import android.widget.ImageView +import com.all.pdfreader.pro.app.R +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class GlideEngine private constructor() : ImageEngine { + + /** + * 加载图片 + */ + override fun loadImage(context: Context, url: String?, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + Glide.with(context) + .load(url) + .into(imageView) + } + + /** + * 按指定大小加载图片 + */ + override fun loadImage( + context: Context, + imageView: ImageView, + url: String?, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView) + } + + /** + * 加载相册目录封面 + */ + override fun loadAlbumCover(context: Context, url: String?, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(CenterCrop(), RoundedCorners(8)) + .placeholder(R.drawable.engine_image_placeholder) + .into(imageView) + } + + /** + * 加载图片列表图片 + */ + override fun loadGridImage(context: Context, url: String?, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + Glide.with(context) + .load(url) + .override(200, 200) + .centerCrop() + .placeholder(R.drawable.engine_image_placeholder) + .into(imageView) + } + + override fun pauseRequests(context: Context) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + Glide.with(context).pauseRequests() + } + + override fun resumeRequests(context: Context) { + if (!ActivityCompatHelper.assertValidRequest(context)) return + Glide.with(context).resumeRequests() + } + + companion object { + @JvmStatic + fun createGlideEngine(): GlideEngine = InstanceHolder.instance + } + + private object InstanceHolder { + val instance = GlideEngine() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt index b938cf4..b009b59 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt @@ -2,6 +2,7 @@ package com.all.pdfreader.pro.app.util import android.content.Context import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.ParcelFileDescriptor import android.util.Log import androidx.core.graphics.createBitmap @@ -10,6 +11,11 @@ import com.shockwave.pdfium.PdfiumCore import com.tom_roush.pdfbox.io.MemoryUsageSetting import com.tom_roush.pdfbox.multipdf.PDFMergerUtility import com.tom_roush.pdfbox.pdmodel.PDDocument +import com.tom_roush.pdfbox.pdmodel.PDPage +import com.tom_roush.pdfbox.pdmodel.PDPageContentStream +import com.tom_roush.pdfbox.pdmodel.common.PDRectangle +import com.tom_roush.pdfbox.pdmodel.graphics.image.JPEGFactory +import com.tom_roush.pdfbox.pdmodel.graphics.image.LosslessFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -44,11 +50,17 @@ object PdfUtils { inputFile: File, dpi: Float = 72f, chunkSize: Int = 5, - thumbWidth: Int = 200 + thumbWidth: Int = 200, + password: String? = null ): Flow = flow { val pdfiumCore = PdfiumCore(context) ParcelFileDescriptor.open(inputFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd -> - val pdfDocument = pdfiumCore.newDocument(fd) + val pdfDocument = if (password.isNullOrEmpty()) { + pdfiumCore.newDocument(fd) + } else { + pdfiumCore.newDocument(fd, password) + } + val pageCount = pdfiumCore.getPageCount(pdfDocument) val pages = List(pageCount) { @@ -208,4 +220,83 @@ object PdfUtils { null } } + + /** + * 图片转 PDF(优化版,安全、快速、低内存) + */ + suspend fun imgToPdfFilesSafe( + inputFiles: List, + outputDir: File, + outputFileName: String, + onProgress: ((current: Int, total: Int) -> Unit)? = null + ): File? = withContext(Dispatchers.IO) { + if (inputFiles.isEmpty()) return@withContext null + + val outputFile = File(outputDir, outputFileName) + if (!outputDir.exists()) outputDir.mkdirs() + + var document: PDDocument? = null + try { + document = PDDocument() + val total = inputFiles.size + + inputFiles.forEachIndexed { index, imageFile -> + try { + //安全加载压缩后的图片 + val options = BitmapFactory.Options().apply { + inPreferredConfig = Bitmap.Config.RGB_565 + } + val bitmap = BitmapFactory.decodeFile(imageFile.absolutePath, options) + ?: return@forEachIndexed + + //按 A4 比例缩放 + val a4Width = PDRectangle.A4.width + val a4Height = PDRectangle.A4.height + val imageRatio = bitmap.width.toFloat() / bitmap.height + val pageRatio = a4Width / a4Height + + val targetWidth: Float + val targetHeight: Float + if (imageRatio > pageRatio) { + targetWidth = a4Width + targetHeight = a4Width / imageRatio + } else { + targetHeight = a4Height + targetWidth = a4Height * imageRatio + } + + val offsetX = (a4Width - targetWidth) / 2 + val offsetY = (a4Height - targetHeight) / 2 + + val page = PDPage(PDRectangle.A4) + document.addPage(page) + + val pdImage = JPEGFactory.createFromImage(document, bitmap) + PDPageContentStream(document, page).use { contentStream -> + contentStream.drawImage(pdImage, offsetX, offsetY, targetWidth, targetHeight) + } + + bitmap.recycle() + + } catch (e: OutOfMemoryError) { + e.printStackTrace() + System.gc() // 手动GC一次(极端情况下) + return@forEachIndexed + } catch (e: Exception) { + e.printStackTrace() + } + + onProgress?.invoke(index + 1, total) + } + + document.save(outputFile) + outputFile + + } catch (e: Exception) { + e.printStackTrace() + null + } finally { + document?.close() + } + } } diff --git a/app/src/main/res/drawable/engine_image_placeholder.xml b/app/src/main/res/drawable/engine_image_placeholder.xml new file mode 100644 index 0000000..8c63cb1 --- /dev/null +++ b/app/src/main/res/drawable/engine_image_placeholder.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/num_checkbox_selector.xml b/app/src/main/res/drawable/num_checkbox_selector.xml new file mode 100644 index 0000000..31c8361 --- /dev/null +++ b/app/src/main/res/drawable/num_checkbox_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/num_oval_normal.xml b/app/src/main/res/drawable/num_oval_normal.xml new file mode 100644 index 0000000..8e6c552 --- /dev/null +++ b/app/src/main/res/drawable/num_oval_normal.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/num_oval_selected.xml b/app/src/main/res/drawable/num_oval_selected.xml new file mode 100644 index 0000000..89c689c --- /dev/null +++ b/app/src/main/res/drawable/num_oval_selected.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tools.xml b/app/src/main/res/layout/fragment_tools.xml index 921f28d..9fa1944 100644 --- a/app/src/main/res/layout/fragment_tools.xml +++ b/app/src/main/res/layout/fragment_tools.xml @@ -35,4 +35,23 @@ android:text="@string/unlock_pdf" /> +