Skip to content

Commit fede5cc

Browse files
empratyushTommy-Geenexus
authored andcommitted
added support for encrypted PDF
based on #17 Signed-off-by: Pratyush <[email protected]> Co-authored-by: Tommy-Geenexus <[email protected]> Co-authored-by: empratyush <[email protected]>
1 parent 1782892 commit fede5cc

File tree

5 files changed

+212
-11
lines changed

5 files changed

+212
-11
lines changed

app/src/main/assets/viewer.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,27 @@ function isTextSelected() {
198198
return window.getSelection().toString() !== "";
199199
}
200200

201-
pdfjsLib.getDocument("https://localhost/placeholder.pdf").promise.then(function(newDoc) {
202-
pdfDoc = newDoc;
203-
channel.setNumPages(pdfDoc.numPages);
204-
pdfDoc.getMetadata().then(function(data) {
205-
channel.setDocumentProperties(JSON.stringify(data.info));
206-
}).catch(function(error) {
207-
console.log("getMetadata error: " + error);
201+
function loadDocument() {
202+
const pdfPassword = channel.getPassword();
203+
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
204+
loadingTask.onPassword = (_, error) => {
205+
if (error === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
206+
channel.showPasswordPrompt();
207+
} else if (error === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
208+
channel.invalidPassword();
209+
}
210+
}
211+
212+
loadingTask.promise.then(function (newDoc) {
213+
pdfDoc = newDoc;
214+
channel.setNumPages(pdfDoc.numPages);
215+
pdfDoc.getMetadata().then(function (data) {
216+
channel.setDocumentProperties(JSON.stringify(data.info));
217+
}).catch(function (error) {
218+
console.log("getMetadata error: " + error);
219+
});
220+
renderPage(channel.getPage(), false, false);
221+
}, function (reason) {
222+
console.error(reason.name + ": " + reason.message);
208223
});
209-
renderPage(channel.getPage(), false, false);
210-
}).catch(function(error) {
211-
console.log("getDocument error: " + error);
212-
});
224+
}

app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,21 @@
3232
import androidx.core.view.ViewCompat;
3333
import androidx.core.view.WindowCompat;
3434
import androidx.core.view.WindowInsetsCompat;
35+
import androidx.fragment.app.Fragment;
3536
import androidx.loader.app.LoaderManager;
3637
import androidx.loader.content.Loader;
3738

3839
import com.google.android.material.snackbar.Snackbar;
3940

4041
import app.grapheneos.pdfviewer.databinding.PdfviewerBinding;
4142
import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
43+
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
4244
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
4345
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
4446

4547
import java.io.IOException;
4648
import java.io.InputStream;
49+
import java.io.FileNotFoundException;
4750
import java.util.HashMap;
4851
import java.util.List;
4952

@@ -54,6 +57,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
5457
private static final String STATE_PAGE = "page";
5558
private static final String STATE_ZOOM_RATIO = "zoomRatio";
5659
private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees";
60+
private static final String STATE_ENCRYPTED_DOCUMENT_PASSWORD = "encrypted_document_password";
5761
private static final String KEY_PROPERTIES = "properties";
5862
private static final int MIN_WEBVIEW_RELEASE = 89;
5963

@@ -110,13 +114,15 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
110114
private float mZoomRatio = 1f;
111115
private int mDocumentOrientationDegrees;
112116
private int mDocumentState;
117+
private String mEncryptedDocumentPassword;
113118
private List<CharSequence> mDocumentProperties;
114119
private InputStream mInputStream;
115120

116121
private PdfviewerBinding binding;
117122
private TextView mTextView;
118123
private Toast mToast;
119124
private Snackbar snackbar;
125+
private PasswordPromptFragment mPasswordPromptFragment;
120126

121127
private final ActivityResultLauncher<Intent> openDocumentLauncher = registerForActivityResult(
122128
new ActivityResultContracts.StartActivityForResult(), result -> {
@@ -127,6 +133,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
127133
mUri = result.getData().getData();
128134
mPage = 1;
129135
mDocumentProperties = null;
136+
mEncryptedDocumentPassword = "";
130137
loadPdf();
131138
invalidateOptionsMenu();
132139
}
@@ -177,6 +184,29 @@ public void setDocumentProperties(final String properties) {
177184
args.putString(KEY_PROPERTIES, properties);
178185
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
179186
}
187+
188+
@JavascriptInterface
189+
public void showPasswordPrompt() {
190+
if (getPasswordPromptFragment().isAdded()) {
191+
getPasswordPromptFragment().dismiss();
192+
}
193+
getPasswordPromptFragment().show(getSupportFragmentManager(), PasswordPromptFragment.class.getName());
194+
}
195+
196+
@JavascriptInterface
197+
public void invalidPassword() {
198+
runOnUiThread(PdfViewer.this::notifyInvalidPassword);
199+
showPasswordPrompt();
200+
}
201+
202+
@JavascriptInterface
203+
public String getPassword() {
204+
return mEncryptedDocumentPassword != null ? mEncryptedDocumentPassword : "";
205+
}
206+
}
207+
208+
private void notifyInvalidPassword() {
209+
snackbar.setText(R.string.password_prompt_invalid_password).show();
180210
}
181211

182212
@Override
@@ -241,6 +271,12 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque
241271
Log.d(TAG, "path " + path);
242272

243273
if ("/placeholder.pdf".equals(path)) {
274+
maybeCloseInputStream();
275+
try {
276+
mInputStream = getContentResolver().openInputStream(mUri);
277+
} catch (FileNotFoundException ignored) {
278+
snackbar.setText(R.string.io_error).show();
279+
}
244280
return new WebResourceResponse("application/pdf", null, mInputStream);
245281
}
246282

@@ -274,6 +310,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request
274310
public void onPageFinished(WebView view, String url) {
275311
mDocumentState = STATE_LOADED;
276312
invalidateOptionsMenu();
313+
loadPdfWithPassword(mEncryptedDocumentPassword);
277314
}
278315
});
279316

@@ -341,6 +378,7 @@ public void onZoomEnd() {
341378
mPage = savedInstanceState.getInt(STATE_PAGE);
342379
mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO);
343380
mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES);
381+
mEncryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD);
344382
}
345383

346384
if (mUri != null) {
@@ -353,6 +391,38 @@ public void onZoomEnd() {
353391
}
354392
}
355393

394+
@Override
395+
protected void onDestroy() {
396+
super.onDestroy();
397+
binding.webview.removeJavascriptInterface("channel");
398+
binding.getRoot().removeView(binding.webview);
399+
binding.webview.destroy();
400+
maybeCloseInputStream();
401+
}
402+
403+
void maybeCloseInputStream() {
404+
InputStream stream = mInputStream;
405+
if (stream == null) {
406+
return;
407+
}
408+
mInputStream = null;
409+
try {
410+
stream.close();
411+
} catch (IOException ignored) {}
412+
}
413+
414+
private PasswordPromptFragment getPasswordPromptFragment() {
415+
if (mPasswordPromptFragment == null) {
416+
final Fragment fragment = getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName());
417+
if (fragment != null) {
418+
mPasswordPromptFragment = (PasswordPromptFragment) fragment;
419+
} else {
420+
mPasswordPromptFragment = new PasswordPromptFragment();
421+
}
422+
}
423+
return mPasswordPromptFragment;
424+
}
425+
356426
@Override
357427
protected void onResume() {
358428
super.onResume();
@@ -403,10 +473,17 @@ private void loadPdf() {
403473
return;
404474
}
405475

476+
mDocumentState = 0;
406477
showSystemUi();
478+
invalidateOptionsMenu();
407479
binding.webview.loadUrl("https://localhost/viewer.html");
408480
}
409481

482+
public void loadPdfWithPassword(final String password) {
483+
mEncryptedDocumentPassword = password;
484+
binding.webview.evaluateJavascript("loadDocument()", null);
485+
}
486+
410487
private void renderPage(final int zoom) {
411488
binding.webview.evaluateJavascript("onRenderPage(" + zoom + ")", null);
412489
}
@@ -503,6 +580,7 @@ public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
503580
savedInstanceState.putInt(STATE_PAGE, mPage);
504581
savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio);
505582
savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees);
583+
savedInstanceState.putString(STATE_ENCRYPTED_DOCUMENT_PASSWORD, mEncryptedDocumentPassword);
506584
}
507585

508586
private void showPageNumber() {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package app.grapheneos.pdfviewer.fragment
2+
3+
import android.app.Dialog
4+
import android.content.DialogInterface
5+
import android.os.Bundle
6+
import android.text.Editable
7+
import android.text.TextUtils
8+
import android.text.TextWatcher
9+
import android.view.LayoutInflater
10+
import android.view.WindowManager
11+
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
12+
import androidx.appcompat.app.AlertDialog
13+
import androidx.fragment.app.DialogFragment
14+
import app.grapheneos.pdfviewer.PdfViewer
15+
import app.grapheneos.pdfviewer.R
16+
import app.grapheneos.pdfviewer.databinding.PasswordDialogFragmentBinding
17+
import com.google.android.material.textfield.TextInputEditText
18+
19+
class PasswordPromptFragment : DialogFragment() {
20+
21+
private lateinit var passwordEditText : TextInputEditText
22+
23+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
24+
val passwordPrompt = AlertDialog.Builder(requireContext())
25+
val passwordDialogFragmentBinding =
26+
PasswordDialogFragmentBinding.inflate(LayoutInflater.from(requireContext()))
27+
passwordEditText = passwordDialogFragmentBinding.pdfPasswordEditText
28+
passwordPrompt.setView(passwordDialogFragmentBinding.root)
29+
passwordEditText.addTextChangedListener(object : TextWatcher {
30+
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
31+
override fun onTextChanged(input: CharSequence, i: Int, i1: Int, i2: Int) {
32+
updatePositiveButton()
33+
}
34+
override fun afterTextChanged(editable: Editable) {}
35+
})
36+
passwordEditText.setOnEditorActionListener { _, actionId, _ ->
37+
if (actionId != IME_ACTION_DONE) return@setOnEditorActionListener false
38+
sendPassword()
39+
true
40+
}
41+
passwordPrompt.setPositiveButton(R.string.open) { _, _ -> sendPassword() }
42+
passwordPrompt.setNegativeButton(R.string.cancel, null)
43+
val dialog = passwordPrompt.create()
44+
dialog.setCanceledOnTouchOutside(false)
45+
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
46+
return dialog
47+
}
48+
49+
private fun updatePositiveButton() {
50+
val btn = (dialog as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
51+
btn.isEnabled = passwordEditText.text?.isNotEmpty() ?: false
52+
}
53+
54+
private fun sendPassword() {
55+
val password = passwordEditText.text.toString()
56+
if (!TextUtils.isEmpty(password)) {
57+
(activity as PdfViewer).loadPdfWithPassword(password)
58+
dialog?.dismiss()
59+
}
60+
}
61+
62+
override fun onStart() {
63+
super.onStart()
64+
updatePositiveButton()
65+
passwordEditText.requestFocus()
66+
}
67+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="wrap_content"
5+
android:layout_height="wrap_content"
6+
android:orientation="vertical">
7+
8+
<TextView
9+
android:layout_width="match_parent"
10+
android:layout_height="wrap_content"
11+
android:layout_marginStart="24dp"
12+
android:layout_marginTop="24dp"
13+
android:layout_marginEnd="24dp"
14+
android:layout_marginBottom="20dp"
15+
android:text="@string/password_prompt_description"
16+
android:textColor="?android:attr/colorAccent"
17+
android:textSize="20sp" />
18+
19+
<com.google.android.material.textfield.TextInputLayout
20+
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
21+
android:layout_width="match_parent"
22+
android:layout_height="wrap_content"
23+
app:hintEnabled="false"
24+
app:passwordToggleEnabled="true"
25+
app:passwordToggleTint="?android:attr/colorAccent">
26+
27+
<com.google.android.material.textfield.TextInputEditText
28+
android:id="@+id/pdf_password_edit_text"
29+
android:layout_width="match_parent"
30+
android:layout_height="wrap_content"
31+
android:layout_marginStart="24dp"
32+
android:layout_marginEnd="24dp"
33+
android:layout_marginBottom="24dp"
34+
android:hint="@string/password_prompt_hint"
35+
android:inputType="textPassword" />
36+
37+
</com.google.android.material.textfield.TextInputLayout>
38+
</LinearLayout>

app/src/main/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@
2424

2525
<string name="webview_out_of_date_title">WebView out-of-date</string>
2626
<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>
27+
28+
<string name="password_prompt_hint">Password</string>
29+
<string name="password_prompt_description">Enter the password to decrypt this PDF file</string>
30+
<string name="password_prompt_invalid_password">Invalid password</string>
31+
<string name="open">Open</string>
32+
<string name="cancel">Cancel</string>
2733
</resources>

0 commit comments

Comments
 (0)