From 87c71ddac2da57929da99b1ceeb9f4ea5bdb11a9 Mon Sep 17 00:00:00 2001 From: Pratyush Date: Sun, 1 May 2022 20:16:40 +0530 Subject: [PATCH] added support for encrypted PDF based on https://github.com/GrapheneOS/PdfViewer/pull/17 Signed-off-by: Pratyush Co-authored-by: Tommy-Geenexus Co-authored-by: empratyush --- app/src/main/assets/viewer.js | 34 +++++--- .../app/grapheneos/pdfviewer/PdfViewer.java | 78 +++++++++++++++++++ .../fragment/PasswordPromptFragment.kt | 67 ++++++++++++++++ .../res/layout/password_dialog_fragment.xml | 38 +++++++++ app/src/main/res/values/strings.xml | 6 ++ 5 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/app/grapheneos/pdfviewer/fragment/PasswordPromptFragment.kt create mode 100644 app/src/main/res/layout/password_dialog_fragment.xml diff --git a/app/src/main/assets/viewer.js b/app/src/main/assets/viewer.js index c5e4ef2..0e04afa 100644 --- a/app/src/main/assets/viewer.js +++ b/app/src/main/assets/viewer.js @@ -198,15 +198,27 @@ function isTextSelected() { return window.getSelection().toString() !== ""; } -pdfjsLib.getDocument("https://localhost/placeholder.pdf").promise.then(function(newDoc) { - pdfDoc = newDoc; - channel.setNumPages(pdfDoc.numPages); - pdfDoc.getMetadata().then(function(data) { - channel.setDocumentProperties(JSON.stringify(data.info)); - }).catch(function(error) { - console.log("getMetadata error: " + error); +function loadDocument() { + const pdfPassword = channel.getPassword(); + const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword }); + loadingTask.onPassword = (_, error) => { + if (error === pdfjsLib.PasswordResponses.NEED_PASSWORD) { + channel.showPasswordPrompt(); + } else if (error === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) { + channel.invalidPassword(); + } + } + + loadingTask.promise.then(function (newDoc) { + pdfDoc = newDoc; + channel.setNumPages(pdfDoc.numPages); + pdfDoc.getMetadata().then(function (data) { + channel.setDocumentProperties(JSON.stringify(data.info)); + }).catch(function (error) { + console.log("getMetadata error: " + error); + }); + renderPage(channel.getPage(), false, false); + }, function (reason) { + console.error(reason.name + ": " + reason.message); }); - renderPage(channel.getPage(), false, false); -}).catch(function(error) { - console.log("getDocument error: " + error); -}); +} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 2a1d873..ceb67c2 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -32,6 +32,7 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.Fragment; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; @@ -39,11 +40,13 @@ import com.google.android.material.snackbar.Snackbar; import app.grapheneos.pdfviewer.databinding.PdfviewerBinding; import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment; +import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment; import app.grapheneos.pdfviewer.fragment.JumpToPageFragment; import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader; import java.io.IOException; import java.io.InputStream; +import java.io.FileNotFoundException; import java.util.HashMap; import java.util.List; @@ -54,6 +57,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private static final String STATE_PAGE = "page"; private static final String STATE_ZOOM_RATIO = "zoomRatio"; private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees"; + private static final String STATE_ENCRYPTED_DOCUMENT_PASSWORD = "encrypted_document_password"; private static final String KEY_PROPERTIES = "properties"; private static final int MIN_WEBVIEW_RELEASE = 89; @@ -110,6 +114,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private float mZoomRatio = 1f; private int mDocumentOrientationDegrees; private int mDocumentState; + private String mEncryptedDocumentPassword; private List mDocumentProperties; private InputStream mInputStream; @@ -117,6 +122,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private TextView mTextView; private Toast mToast; private Snackbar snackbar; + private PasswordPromptFragment mPasswordPromptFragment; private final ActivityResultLauncher openDocumentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { @@ -127,6 +133,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader mUri = result.getData().getData(); mPage = 1; mDocumentProperties = null; + mEncryptedDocumentPassword = ""; loadPdf(); invalidateOptionsMenu(); } @@ -177,6 +184,29 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader args.putString(KEY_PROPERTIES, properties); runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this)); } + + @JavascriptInterface + public void showPasswordPrompt() { + if (getPasswordPromptFragment().isAdded()) { + getPasswordPromptFragment().dismiss(); + } + getPasswordPromptFragment().show(getSupportFragmentManager(), PasswordPromptFragment.class.getName()); + } + + @JavascriptInterface + public void invalidPassword() { + runOnUiThread(PdfViewer.this::notifyInvalidPassword); + showPasswordPrompt(); + } + + @JavascriptInterface + public String getPassword() { + return mEncryptedDocumentPassword != null ? mEncryptedDocumentPassword : ""; + } + } + + private void notifyInvalidPassword() { + snackbar.setText(R.string.password_prompt_invalid_password).show(); } @Override @@ -241,6 +271,12 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader Log.d(TAG, "path " + path); if ("/placeholder.pdf".equals(path)) { + maybeCloseInputStream(); + try { + mInputStream = getContentResolver().openInputStream(mUri); + } catch (FileNotFoundException ignored) { + snackbar.setText(R.string.io_error).show(); + } return new WebResourceResponse("application/pdf", null, mInputStream); } @@ -274,6 +310,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader public void onPageFinished(WebView view, String url) { mDocumentState = STATE_LOADED; invalidateOptionsMenu(); + loadPdfWithPassword(mEncryptedDocumentPassword); } }); @@ -341,6 +378,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader mPage = savedInstanceState.getInt(STATE_PAGE); mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO); mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES); + mEncryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD); } if (mUri != null) { @@ -353,6 +391,38 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader } } + @Override + protected void onDestroy() { + super.onDestroy(); + binding.webview.removeJavascriptInterface("channel"); + binding.getRoot().removeView(binding.webview); + binding.webview.destroy(); + maybeCloseInputStream(); + } + + void maybeCloseInputStream() { + InputStream stream = mInputStream; + if (stream == null) { + return; + } + mInputStream = null; + try { + stream.close(); + } catch (IOException ignored) {} + } + + private PasswordPromptFragment getPasswordPromptFragment() { + if (mPasswordPromptFragment == null) { + final Fragment fragment = getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName()); + if (fragment != null) { + mPasswordPromptFragment = (PasswordPromptFragment) fragment; + } else { + mPasswordPromptFragment = new PasswordPromptFragment(); + } + } + return mPasswordPromptFragment; + } + @Override protected void onResume() { super.onResume(); @@ -403,10 +473,17 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader return; } + mDocumentState = 0; showSystemUi(); + invalidateOptionsMenu(); binding.webview.loadUrl("https://localhost/viewer.html"); } + public void loadPdfWithPassword(final String password) { + mEncryptedDocumentPassword = password; + binding.webview.evaluateJavascript("loadDocument()", null); + } + private void renderPage(final int zoom) { binding.webview.evaluateJavascript("onRenderPage(" + zoom + ")", null); } @@ -503,6 +580,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader savedInstanceState.putInt(STATE_PAGE, mPage); savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio); savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees); + savedInstanceState.putString(STATE_ENCRYPTED_DOCUMENT_PASSWORD, mEncryptedDocumentPassword); } private void showPageNumber() { diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/PasswordPromptFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/PasswordPromptFragment.kt new file mode 100644 index 0000000..545b5f0 --- /dev/null +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/PasswordPromptFragment.kt @@ -0,0 +1,67 @@ +package app.grapheneos.pdfviewer.fragment + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.WindowManager +import android.view.inputmethod.EditorInfo.IME_ACTION_DONE +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import app.grapheneos.pdfviewer.PdfViewer +import app.grapheneos.pdfviewer.R +import app.grapheneos.pdfviewer.databinding.PasswordDialogFragmentBinding +import com.google.android.material.textfield.TextInputEditText + +class PasswordPromptFragment : DialogFragment() { + + private lateinit var passwordEditText : TextInputEditText + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val passwordPrompt = AlertDialog.Builder(requireContext()) + val passwordDialogFragmentBinding = + PasswordDialogFragmentBinding.inflate(LayoutInflater.from(requireContext())) + passwordEditText = passwordDialogFragmentBinding.pdfPasswordEditText + passwordPrompt.setView(passwordDialogFragmentBinding.root) + passwordEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun onTextChanged(input: CharSequence, i: Int, i1: Int, i2: Int) { + updatePositiveButton() + } + override fun afterTextChanged(editable: Editable) {} + }) + passwordEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId != IME_ACTION_DONE) return@setOnEditorActionListener false + sendPassword() + true + } + passwordPrompt.setPositiveButton(R.string.open) { _, _ -> sendPassword() } + passwordPrompt.setNegativeButton(R.string.cancel, null) + val dialog = passwordPrompt.create() + dialog.setCanceledOnTouchOutside(false) + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + return dialog + } + + private fun updatePositiveButton() { + val btn = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE) + btn.isEnabled = passwordEditText.text?.isNotEmpty() ?: false + } + + private fun sendPassword() { + val password = passwordEditText.text.toString() + if (!TextUtils.isEmpty(password)) { + (activity as PdfViewer).loadPdfWithPassword(password) + dialog?.dismiss() + } + } + + override fun onStart() { + super.onStart() + updatePositiveButton() + passwordEditText.requestFocus() + } +} diff --git a/app/src/main/res/layout/password_dialog_fragment.xml b/app/src/main/res/layout/password_dialog_fragment.xml new file mode 100644 index 0000000..a2448fd --- /dev/null +++ b/app/src/main/res/layout/password_dialog_fragment.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c750141..004fbb6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,4 +24,10 @@ WebView out-of-date Your current WebView version is %1$d. The WebView should be at least version %2$d for the PDF Viewer to work. + + Password + Enter the password to decrypt this PDF file + Invalid password + Open + Cancel