added support for encrypted PDF
based on https://github.com/GrapheneOS/PdfViewer/pull/17 Signed-off-by: Pratyush <codelab@pratyush.dev> Co-authored-by: Tommy-Geenexus <tomgapplicationsdevelopment@gmail.com> Co-authored-by: empratyush <codelab@pratyush.dev>
This commit is contained in:
parent
7dab1cdc91
commit
87c71ddac2
@ -198,15 +198,27 @@ function isTextSelected() {
|
|||||||
return window.getSelection().toString() !== "";
|
return window.getSelection().toString() !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfjsLib.getDocument("https://localhost/placeholder.pdf").promise.then(function(newDoc) {
|
function loadDocument() {
|
||||||
pdfDoc = newDoc;
|
const pdfPassword = channel.getPassword();
|
||||||
channel.setNumPages(pdfDoc.numPages);
|
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
|
||||||
pdfDoc.getMetadata().then(function(data) {
|
loadingTask.onPassword = (_, error) => {
|
||||||
channel.setDocumentProperties(JSON.stringify(data.info));
|
if (error === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
|
||||||
}).catch(function(error) {
|
channel.showPasswordPrompt();
|
||||||
console.log("getMetadata error: " + error);
|
} 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);
|
|
||||||
});
|
|
||||||
|
@ -32,6 +32,7 @@ import androidx.core.graphics.Insets;
|
|||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowCompat;
|
import androidx.core.view.WindowCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import androidx.loader.content.Loader;
|
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.databinding.PdfviewerBinding;
|
||||||
import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
|
import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
|
||||||
|
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
|
||||||
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
|
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
|
||||||
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
|
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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_PAGE = "page";
|
||||||
private static final String STATE_ZOOM_RATIO = "zoomRatio";
|
private static final String STATE_ZOOM_RATIO = "zoomRatio";
|
||||||
private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees";
|
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 String KEY_PROPERTIES = "properties";
|
||||||
private static final int MIN_WEBVIEW_RELEASE = 89;
|
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 float mZoomRatio = 1f;
|
||||||
private int mDocumentOrientationDegrees;
|
private int mDocumentOrientationDegrees;
|
||||||
private int mDocumentState;
|
private int mDocumentState;
|
||||||
|
private String mEncryptedDocumentPassword;
|
||||||
private List<CharSequence> mDocumentProperties;
|
private List<CharSequence> mDocumentProperties;
|
||||||
private InputStream mInputStream;
|
private InputStream mInputStream;
|
||||||
|
|
||||||
@ -117,6 +122,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||||||
private TextView mTextView;
|
private TextView mTextView;
|
||||||
private Toast mToast;
|
private Toast mToast;
|
||||||
private Snackbar snackbar;
|
private Snackbar snackbar;
|
||||||
|
private PasswordPromptFragment mPasswordPromptFragment;
|
||||||
|
|
||||||
private final ActivityResultLauncher<Intent> openDocumentLauncher = registerForActivityResult(
|
private final ActivityResultLauncher<Intent> openDocumentLauncher = registerForActivityResult(
|
||||||
new ActivityResultContracts.StartActivityForResult(), result -> {
|
new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||||
@ -127,6 +133,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||||||
mUri = result.getData().getData();
|
mUri = result.getData().getData();
|
||||||
mPage = 1;
|
mPage = 1;
|
||||||
mDocumentProperties = null;
|
mDocumentProperties = null;
|
||||||
|
mEncryptedDocumentPassword = "";
|
||||||
loadPdf();
|
loadPdf();
|
||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
}
|
}
|
||||||
@ -177,6 +184,29 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||||||
args.putString(KEY_PROPERTIES, properties);
|
args.putString(KEY_PROPERTIES, properties);
|
||||||
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
|
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
|
@Override
|
||||||
@ -241,6 +271,12 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||||||
Log.d(TAG, "path " + path);
|
Log.d(TAG, "path " + path);
|
||||||
|
|
||||||
if ("/placeholder.pdf".equals(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);
|
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) {
|
public void onPageFinished(WebView view, String url) {
|
||||||
mDocumentState = STATE_LOADED;
|
mDocumentState = STATE_LOADED;
|
||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
|
loadPdfWithPassword(mEncryptedDocumentPassword);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -341,6 +378,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||||||
mPage = savedInstanceState.getInt(STATE_PAGE);
|
mPage = savedInstanceState.getInt(STATE_PAGE);
|
||||||
mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO);
|
mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO);
|
||||||
mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES);
|
mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES);
|
||||||
|
mEncryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mUri != null) {
|
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
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@ -403,10 +473,17 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mDocumentState = 0;
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
|
invalidateOptionsMenu();
|
||||||
binding.webview.loadUrl("https://localhost/viewer.html");
|
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) {
|
private void renderPage(final int zoom) {
|
||||||
binding.webview.evaluateJavascript("onRenderPage(" + zoom + ")", null);
|
binding.webview.evaluateJavascript("onRenderPage(" + zoom + ")", null);
|
||||||
}
|
}
|
||||||
@ -503,6 +580,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
|
|||||||
savedInstanceState.putInt(STATE_PAGE, mPage);
|
savedInstanceState.putInt(STATE_PAGE, mPage);
|
||||||
savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio);
|
savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio);
|
||||||
savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees);
|
savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees);
|
||||||
|
savedInstanceState.putString(STATE_ENCRYPTED_DOCUMENT_PASSWORD, mEncryptedDocumentPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPageNumber() {
|
private void showPageNumber() {
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
38
app/src/main/res/layout/password_dialog_fragment.xml
Normal file
38
app/src/main/res/layout/password_dialog_fragment.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:text="@string/password_prompt_description"
|
||||||
|
android:textColor="?android:attr/colorAccent"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:hintEnabled="false"
|
||||||
|
app:passwordToggleEnabled="true"
|
||||||
|
app:passwordToggleTint="?android:attr/colorAccent">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/pdf_password_edit_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:hint="@string/password_prompt_hint"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
</LinearLayout>
|
@ -24,4 +24,10 @@
|
|||||||
|
|
||||||
<string name="webview_out_of_date_title">WebView out-of-date</string>
|
<string name="webview_out_of_date_title">WebView out-of-date</string>
|
||||||
<string name="webview_out_of_date_message">Your current WebView version is %1$d. The WebView should be at least version %2$d for the PDF Viewer to work.</string>
|
<string name="webview_out_of_date_message">Your current WebView version is %1$d. The WebView should be at least version %2$d for the PDF Viewer to work.</string>
|
||||||
|
|
||||||
|
<string name="password_prompt_hint">Password</string>
|
||||||
|
<string name="password_prompt_description">Enter the password to decrypt this PDF file</string>
|
||||||
|
<string name="password_prompt_invalid_password">Invalid password</string>
|
||||||
|
<string name="open">Open</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user