From 82334e77a7742c984a5a2d7cee800fd9eb533e4d Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:27:32 +0100 Subject: [PATCH 01/10] Remove the usage of Hungarian Notation --- .../app/grapheneos/pdfviewer/PdfViewer.java | 200 +++++++++--------- .../fragment/DocumentPropertiesFragment.kt | 6 +- .../pdfviewer/fragment/JumpToPageFragment.kt | 26 +-- .../DocumentPropertiesAsyncTaskLoader.java | 18 +- .../loader/DocumentPropertiesLoader.kt | 4 +- 5 files changed, 127 insertions(+), 127 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 936f70851..1d7d10020 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -119,23 +119,23 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private static final int PADDING = 10; private boolean webViewCrashed; - private Uri mUri; - public int mPage; - public int mNumPages; - private float mZoomRatio = 1f; - private float mZoomFocusX = 0f; - private float mZoomFocusY = 0f; - private int mDocumentOrientationDegrees; - private int mDocumentState; - private String mEncryptedDocumentPassword; - private List mDocumentProperties; - private InputStream mInputStream; + private Uri uri; + public int page; + public int numPages; + private float zoomRatio = 1f; + private float zoomFocusX = 0f; + private float zoomFocusY = 0f; + private int documentOrientationDegrees; + private int documentState; + private String encryptedDocumentPassword; + private List documentProperties; + private InputStream inputStream; private PdfviewerBinding binding; - private TextView mTextView; - private Toast mToast; + private TextView textView; + private Toast toast; private Snackbar snackbar; - private PasswordPromptFragment mPasswordPromptFragment; + private PasswordPromptFragment passwordPromptFragment; public PdfViewModel viewModel; private final ActivityResultLauncher openDocumentLauncher = registerForActivityResult( @@ -144,10 +144,10 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader if (result.getResultCode() != RESULT_OK) return; Intent resultData = result.getData(); if (resultData != null) { - mUri = result.getData().getData(); - mPage = 1; - mDocumentProperties = null; - mEncryptedDocumentPassword = ""; + uri = result.getData().getData(); + page = 1; + documentProperties = null; + encryptedDocumentPassword = ""; viewModel.clearOutline(); loadPdf(); invalidateOptionsMenu(); @@ -180,17 +180,17 @@ public void setDocumentOutline(final String outline) { @JavascriptInterface public int getPage() { - return mPage; + return page; } @JavascriptInterface public float getZoomRatio() { - return mZoomRatio; + return zoomRatio; } @JavascriptInterface public void setZoomRatio(final float ratio) { - mZoomRatio = Math.max(Math.min(ratio, MAX_ZOOM_RATIO), MIN_ZOOM_RATIO); + zoomRatio = Math.max(Math.min(ratio, MAX_ZOOM_RATIO), MIN_ZOOM_RATIO); } @JavascriptInterface @@ -200,12 +200,12 @@ public int getMaxRenderPixels() { @JavascriptInterface public float getZoomFocusX() { - return mZoomFocusX; + return zoomFocusX; } @JavascriptInterface public float getZoomFocusY() { - return mZoomFocusY; + return zoomFocusY; } @JavascriptInterface @@ -220,18 +220,18 @@ public float getMaxZoomRatio() { @JavascriptInterface public int getDocumentOrientationDegrees() { - return mDocumentOrientationDegrees; + return documentOrientationDegrees; } @JavascriptInterface public void setNumPages(int numPages) { - mNumPages = numPages; + PdfViewer.this.numPages = numPages; runOnUiThread(PdfViewer.this::invalidateOptionsMenu); } @JavascriptInterface public void setDocumentProperties(final String properties) { - if (mDocumentProperties != null) { + if (documentProperties != null) { throw new SecurityException("mDocumentProperties not null"); } @@ -263,7 +263,7 @@ public void onLoaded() { @JavascriptInterface public String getPassword() { - return mEncryptedDocumentPassword != null ? mEncryptedDocumentPassword : ""; + return encryptedDocumentPassword != null ? encryptedDocumentPassword : ""; } } @@ -351,8 +351,8 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque if ("/placeholder.pdf".equals(path)) { maybeCloseInputStream(); try { - mInputStream = getContentResolver().openInputStream(mUri); - if (mInputStream == null) { + inputStream = getContentResolver().openInputStream(uri); + if (inputStream == null) { throw new FileNotFoundException(); } } catch (final FileNotFoundException | IllegalArgumentException | @@ -360,7 +360,7 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque snackbar.setText(R.string.error_while_opening).show(); return null; } - return new WebResourceResponse("application/pdf", null, mInputStream); + return new WebResourceResponse("application/pdf", null, inputStream); } if ("/viewer/index.html".equals(path)) { @@ -413,9 +413,9 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request @Override public void onPageFinished(WebView view, String url) { - mDocumentState = STATE_LOADED; + documentState = STATE_LOADED; invalidateOptionsMenu(); - loadPdfWithPassword(mEncryptedDocumentPassword); + loadPdfWithPassword(encryptedDocumentPassword); } @Override @@ -435,7 +435,7 @@ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) new GestureHelper.GestureListener() { @Override public boolean onTapUp() { - if (mUri != null) { + if (uri != null) { binding.webview.evaluateJavascript("isTextSelected()", selection -> { if (!Boolean.parseBoolean(selection)) { if (getSupportActionBar().isShowing()) { @@ -461,11 +461,11 @@ public void onZoomEnd() { } }); - mTextView = new TextView(this); - mTextView.setBackgroundColor(Color.DKGRAY); - mTextView.setTextColor(ColorStateList.valueOf(Color.WHITE)); - mTextView.setTextSize(18); - mTextView.setPadding(PADDING, 0, PADDING, 0); + textView = new TextView(this); + textView.setBackgroundColor(Color.DKGRAY); + textView.setTextColor(ColorStateList.valueOf(Color.WHITE)); + textView.setTextSize(18); + textView.setPadding(PADDING, 0, PADDING, 0); // If loaders are not being initialized in onCreate(), the result will not be delivered // after orientation change (See FragmentHostCallback), thus initialize the @@ -484,23 +484,23 @@ public void onZoomEnd() { if (type == null) { Log.w(TAG, "MIME type is null, but we'll try to load it anyway"); } - mUri = intent.getData(); - mPage = 1; + uri = intent.getData(); + page = 1; } if (savedInstanceState != null) { webViewCrashed = savedInstanceState.getBoolean(STATE_WEBVIEW_CRASHED); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - mUri = savedInstanceState.getParcelable(STATE_URI, Uri.class); + uri = savedInstanceState.getParcelable(STATE_URI, Uri.class); } else { @SuppressWarnings("deprecation") final Uri uri = savedInstanceState.getParcelable(STATE_URI); - mUri = uri; + this.uri = uri; } - 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); + page = savedInstanceState.getInt(STATE_PAGE); + zoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO); + documentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES); + encryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD); } binding.webviewAlertReload.setOnClickListener(v -> { @@ -510,8 +510,8 @@ public void onZoomEnd() { if (webViewCrashed) { showWebViewCrashed(); - } else if (mUri != null) { - if ("file".equals(mUri.getScheme())) { + } else if (uri != null) { + if ("file".equals(uri.getScheme())) { snackbar.setText(R.string.legacy_file_uri).show(); return; } @@ -534,26 +534,26 @@ protected void onDestroy() { } void maybeCloseInputStream() { - InputStream stream = mInputStream; + InputStream stream = inputStream; if (stream == null) { return; } - mInputStream = null; + inputStream = null; try { stream.close(); } catch (IOException ignored) {} } private PasswordPromptFragment getPasswordPromptFragment() { - if (mPasswordPromptFragment == null) { + if (passwordPromptFragment == null) { final Fragment fragment = getSupportFragmentManager().findFragmentByTag(PasswordPromptFragment.class.getName()); if (fragment != null) { - mPasswordPromptFragment = (PasswordPromptFragment) fragment; + passwordPromptFragment = (PasswordPromptFragment) fragment; } else { - mPasswordPromptFragment = new PasswordPromptFragment(); + passwordPromptFragment = new PasswordPromptFragment(); } } - return mPasswordPromptFragment; + return passwordPromptFragment; } private void setToolbarTitleWithDocumentName() { @@ -593,12 +593,12 @@ private int getWebViewRelease() { @NonNull @Override public Loader> onCreateLoader(int id, Bundle args) { - return new DocumentPropertiesAsyncTaskLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri); + return new DocumentPropertiesAsyncTaskLoader(this, args.getString(KEY_PROPERTIES), numPages, uri); } @Override public void onLoadFinished(@NonNull Loader> loader, List data) { - mDocumentProperties = data; + documentProperties = data; invalidateOptionsMenu(); setToolbarTitleWithDocumentName(); LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID); @@ -606,18 +606,18 @@ public void onLoadFinished(@NonNull Loader> loader, List> loader) { - mDocumentProperties = null; + documentProperties = null; } private void loadPdf() { - mDocumentState = 0; + documentState = 0; showSystemUi(); invalidateOptionsMenu(); binding.webview.loadUrl("https://localhost/viewer/index.html"); } public void loadPdfWithPassword(final String password) { - mEncryptedDocumentPassword = password; + encryptedDocumentPassword = password; binding.webview.evaluateJavascript("loadDocument()", null); } @@ -626,9 +626,9 @@ private void renderPage(final int zoom) { } private void documentOrientationChanged(final int orientationDegreesOffset) { - mDocumentOrientationDegrees = (mDocumentOrientationDegrees + orientationDegreesOffset) % 360; - if (mDocumentOrientationDegrees < 0) { - mDocumentOrientationDegrees += 360; + documentOrientationDegrees = (documentOrientationDegrees + orientationDegreesOffset) % 360; + if (documentOrientationDegrees < 0) { + documentOrientationDegrees += 360; } renderPage(0); } @@ -641,10 +641,10 @@ private void openDocument() { } private void shareDocument() { - if (mUri != null) { + if (uri != null) { Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setDataAndTypeAndNormalize(mUri, "application/pdf"); - shareIntent.putExtra(Intent.EXTRA_STREAM, mUri); + shareIntent.setDataAndTypeAndNormalize(uri, "application/pdf"); + shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, getString(R.string.action_share))); } else { @@ -653,9 +653,9 @@ private void shareDocument() { } private void zoom(float scaleFactor, float focusX, float focusY, boolean end) { - mZoomRatio = Math.min(Math.max(mZoomRatio * scaleFactor, MIN_ZOOM_RATIO), MAX_ZOOM_RATIO); - mZoomFocusX = focusX; - mZoomFocusY = focusY; + zoomRatio = Math.min(Math.max(zoomRatio * scaleFactor, MIN_ZOOM_RATIO), MAX_ZOOM_RATIO); + zoomFocusX = focusX; + zoomFocusY = focusY; renderPage(end ? 1 : 2); invalidateOptionsMenu(); } @@ -672,8 +672,8 @@ private static void enableDisableMenuItem(MenuItem item, boolean enable) { } public void onJumpToPageInDocument(final int selected_page) { - if (selected_page >= 1 && selected_page <= mNumPages && mPage != selected_page) { - mPage = selected_page; + if (selected_page >= 1 && selected_page <= numPages && page != selected_page) { + page = selected_page; renderPage(0); showPageNumber(); invalidateOptionsMenu(); @@ -694,23 +694,23 @@ private void hideSystemUi() { public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); savedInstanceState.putBoolean(STATE_WEBVIEW_CRASHED, webViewCrashed); - savedInstanceState.putParcelable(STATE_URI, mUri); - 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); + savedInstanceState.putParcelable(STATE_URI, uri); + savedInstanceState.putInt(STATE_PAGE, page); + savedInstanceState.putFloat(STATE_ZOOM_RATIO, zoomRatio); + savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, documentOrientationDegrees); + savedInstanceState.putString(STATE_ENCRYPTED_DOCUMENT_PASSWORD, encryptedDocumentPassword); } private void showPageNumber() { - if (mToast != null) { - mToast.cancel(); + if (toast != null) { + toast.cancel(); } - mTextView.setText(String.format("%s/%s", mPage, mNumPages)); - mToast = new Toast(this); - mToast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING); - mToast.setDuration(Toast.LENGTH_SHORT); - mToast.setView(mTextView); - mToast.show(); + textView.setText(String.format("%s/%s", page, numPages)); + toast = new Toast(this); + toast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING); + toast.setDuration(Toast.LENGTH_SHORT); + toast.setView(textView); + toast.show(); } @Override @@ -735,32 +735,32 @@ public boolean onPrepareOptionsMenu(@NonNull Menu menu) { ids.add(R.id.debug_action_toggle_text_layer_visibility); ids.add(R.id.debug_action_crash_webview); } - if (mDocumentState < STATE_LOADED) { + if (documentState < STATE_LOADED) { for (final int id : ids) { final MenuItem item = menu.findItem(id); if (item.isVisible()) { item.setVisible(false); } } - } else if (mDocumentState == STATE_LOADED) { + } else if (documentState == STATE_LOADED) { for (final int id : ids) { final MenuItem item = menu.findItem(id); if (!item.isVisible()) { item.setVisible(true); } } - mDocumentState = STATE_END; + documentState = STATE_END; } enableDisableMenuItem(menu.findItem(R.id.action_open), !webViewCrashed && getWebViewRelease() >= MIN_WEBVIEW_RELEASE); - enableDisableMenuItem(menu.findItem(R.id.action_share), mUri != null); - enableDisableMenuItem(menu.findItem(R.id.action_next), mPage < mNumPages); - enableDisableMenuItem(menu.findItem(R.id.action_previous), mPage > 1); - enableDisableMenuItem(menu.findItem(R.id.action_save_as), mUri != null); + enableDisableMenuItem(menu.findItem(R.id.action_share), uri != null); + enableDisableMenuItem(menu.findItem(R.id.action_next), page < numPages); + enableDisableMenuItem(menu.findItem(R.id.action_previous), page > 1); + enableDisableMenuItem(menu.findItem(R.id.action_save_as), uri != null); enableDisableMenuItem(menu.findItem(R.id.action_view_document_properties), - mDocumentProperties != null); + documentProperties != null); menu.findItem(R.id.action_outline).setVisible(viewModel.hasOutline()); @@ -777,16 +777,16 @@ public boolean onPrepareOptionsMenu(@NonNull Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.action_previous) { - onJumpToPageInDocument(mPage - 1); + onJumpToPageInDocument(page - 1); return true; } else if (itemId == R.id.action_next) { - onJumpToPageInDocument(mPage + 1); + onJumpToPageInDocument(page + 1); return true; } else if (itemId == R.id.action_first) { onJumpToPageInDocument(1); return true; } else if (itemId == R.id.action_last) { - onJumpToPageInDocument(mNumPages); + onJumpToPageInDocument(numPages); return true; } else if (itemId == R.id.action_open) { openDocument(); @@ -799,7 +799,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } else if (itemId == R.id.action_outline) { OutlineFragment outlineFragment = - OutlineFragment.newInstance(mPage, getCurrentDocumentName()); + OutlineFragment.newInstance(page, getCurrentDocumentName()); getSupportFragmentManager().beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) // fullscreen fragment, since content root view == activity's root view @@ -809,7 +809,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } else if (itemId == R.id.action_view_document_properties) { DocumentPropertiesFragment - .newInstance(mDocumentProperties) + .newInstance(documentProperties) .show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG); return true; } else if (itemId == R.id.action_jump_to_page) { @@ -841,10 +841,10 @@ private void saveDocument() { } private String getCurrentDocumentName() { - if (mDocumentProperties == null || mDocumentProperties.isEmpty()) return ""; + if (documentProperties == null || documentProperties.isEmpty()) return ""; String fileName = ""; String title = ""; - for (CharSequence property : mDocumentProperties) { + for (CharSequence property : documentProperties) { if (property.toString().startsWith("File name:")) { fileName = property.toString().replace("File name:", ""); } @@ -856,8 +856,8 @@ private String getCurrentDocumentName() { } private void saveDocumentAs(final Uri uri) { - try (final InputStream input = getContentResolver().openInputStream(mUri); - final OutputStream output = getContentResolver().openOutputStream(uri)) { + try (final InputStream input = getContentResolver().openInputStream(this.uri); + final OutputStream output = getContentResolver().openOutputStream(uri)) { if (input == null || output == null) { throw new FileNotFoundException(); } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt index 49d175fb5..57bb87b88 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt @@ -10,20 +10,20 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder class DocumentPropertiesFragment : DialogFragment() { // TODO replace with nav args once the `PdfViewer` activity is converted to kotlin - private val mDocumentProperties: List by lazy { + private val documentProperties: List by lazy { requireArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES)?.toList() ?: emptyList() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return MaterialAlertDialogBuilder(requireActivity()) .setPositiveButton(android.R.string.ok, null).apply { - if (mDocumentProperties.isNotEmpty()) { + if (documentProperties.isNotEmpty()) { setTitle(getString(R.string.action_view_document_properties)) setAdapter( ArrayAdapter( requireActivity(), android.R.layout.simple_list_item_1, - mDocumentProperties + documentProperties ), null ) } else { diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt index aaf3d607e..581d3bc76 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt @@ -19,24 +19,24 @@ class JumpToPageFragment : DialogFragment() { private const val STATE_PICKER_MAX = "picker_max" } - private val mPicker: NumberPicker by lazy { NumberPicker(requireActivity()) } + private val picker: NumberPicker by lazy { NumberPicker(requireActivity()) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val viewerActivity: PdfViewer = (requireActivity() as PdfViewer) if (savedInstanceState != null) { - mPicker.minValue = savedInstanceState.getInt(STATE_PICKER_MIN) - mPicker.maxValue = savedInstanceState.getInt(STATE_PICKER_MAX) - mPicker.value = savedInstanceState.getInt(STATE_PICKER_CUR) + picker.minValue = savedInstanceState.getInt(STATE_PICKER_MIN) + picker.maxValue = savedInstanceState.getInt(STATE_PICKER_MAX) + picker.value = savedInstanceState.getInt(STATE_PICKER_CUR) } else { - mPicker.minValue = 1 - mPicker.maxValue = viewerActivity.mNumPages - mPicker.value = viewerActivity.mPage + picker.minValue = 1 + picker.maxValue = viewerActivity.numPages + picker.value = viewerActivity.page } val layout = FrameLayout(requireActivity()) layout.addView( - mPicker, FrameLayout.LayoutParams( + picker, FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER @@ -45,16 +45,16 @@ class JumpToPageFragment : DialogFragment() { return MaterialAlertDialogBuilder(requireActivity()) .setView(layout) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> - mPicker.clearFocus() - viewerActivity.onJumpToPageInDocument(mPicker.value) + picker.clearFocus() + viewerActivity.onJumpToPageInDocument(picker.value) } .setNegativeButton(android.R.string.cancel, null) .create() } override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(STATE_PICKER_MIN, mPicker.minValue) - outState.putInt(STATE_PICKER_MAX, mPicker.maxValue) - outState.putInt(STATE_PICKER_CUR, mPicker.value) + outState.putInt(STATE_PICKER_MIN, picker.minValue) + outState.putInt(STATE_PICKER_MAX, picker.maxValue) + outState.putInt(STATE_PICKER_CUR, picker.value) } } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java index 62d974572..eec2157b9 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java @@ -14,16 +14,16 @@ public class DocumentPropertiesAsyncTaskLoader extends AsyncTaskLoader loadInBackground() { DocumentPropertiesLoader loader = new DocumentPropertiesLoader( getContext(), - mProperties, - mNumPages, - mUri + properties, + numPages, + uri ); return loader.loadAsList(); diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt index 7f98dec53..3a3736f94 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt @@ -17,7 +17,7 @@ class DocumentPropertiesLoader( private val context: Context, private val properties: String, private val numPages: Int, - private val mUri: Uri + private val uri: Uri ) { fun loadAsList(): List { @@ -89,7 +89,7 @@ class DocumentPropertiesLoader( ) context.contentResolver.query( - mUri, + uri, proj, null, null From aa6aaa235d615c5801cc6551991191a7c45aa618 Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:22:13 +0100 Subject: [PATCH 02/10] Update PasswordPromptFragment on UI thread --- .../app/grapheneos/pdfviewer/PdfViewer.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 1d7d10020..35cc8402b 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -242,23 +242,27 @@ public void setDocumentProperties(final String properties) { @JavascriptInterface public void showPasswordPrompt() { - if (!getPasswordPromptFragment().isAdded()){ - getPasswordPromptFragment().show(getSupportFragmentManager(), PasswordPromptFragment.class.getName()); - } + runOnUiThread(() -> { + if (!getPasswordPromptFragment().isAdded()){ + getPasswordPromptFragment().show(getSupportFragmentManager(), PasswordPromptFragment.class.getName()); + } + }); viewModel.passwordMissing(); } @JavascriptInterface public void invalidPassword() { - runOnUiThread(() -> viewModel.invalid()); + viewModel.invalid(); } @JavascriptInterface public void onLoaded() { viewModel.validated(); - if (getPasswordPromptFragment().isAdded()) { - getPasswordPromptFragment().dismiss(); - } + runOnUiThread(() -> { + if (getPasswordPromptFragment().isAdded()) { + getPasswordPromptFragment().dismiss(); + } + }); } @JavascriptInterface From 0b4f3333471162a2a4ffe879505c5a9d45538d3b Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:27:07 +0100 Subject: [PATCH 03/10] Improve thread-safety on mutable fields --- .../app/grapheneos/pdfviewer/PdfViewer.java | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 35cc8402b..29d917236 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -118,17 +118,19 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private static final int STATE_END = 2; private static final int PADDING = 10; + private final Object streamLock = new Object(); + private boolean webViewCrashed; - private Uri uri; - public int page; - public int numPages; - private float zoomRatio = 1f; - private float zoomFocusX = 0f; - private float zoomFocusY = 0f; - private int documentOrientationDegrees; + private volatile Uri uri; + public volatile int page; + public volatile int numPages; + private volatile float zoomRatio = 1f; + private volatile float zoomFocusX = 0f; + private volatile float zoomFocusY = 0f; + private volatile int documentOrientationDegrees; private int documentState; - private String encryptedDocumentPassword; - private List documentProperties; + private volatile String encryptedDocumentPassword; + private volatile List documentProperties; private InputStream inputStream; private PdfviewerBinding binding; @@ -353,18 +355,20 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque Log.d(TAG, "path " + path); if ("/placeholder.pdf".equals(path)) { - maybeCloseInputStream(); - try { - inputStream = getContentResolver().openInputStream(uri); - if (inputStream == null) { - throw new FileNotFoundException(); + synchronized (streamLock) { + maybeCloseInputStream(); + try { + inputStream = getContentResolver().openInputStream(uri); + if (inputStream == null) { + throw new FileNotFoundException(); + } + } catch (final FileNotFoundException | IllegalArgumentException | + IllegalStateException | SecurityException ignored) { + runOnUiThread(() -> snackbar.setText(R.string.error_while_opening).show()); + return null; } - } catch (final FileNotFoundException | IllegalArgumentException | - IllegalStateException | SecurityException ignored) { - snackbar.setText(R.string.error_while_opening).show(); - return null; + return new WebResourceResponse("application/pdf", null, inputStream); } - return new WebResourceResponse("application/pdf", null, inputStream); } if ("/viewer/index.html".equals(path)) { @@ -538,14 +542,16 @@ protected void onDestroy() { } void maybeCloseInputStream() { - InputStream stream = inputStream; - if (stream == null) { - return; - } - inputStream = null; - try { - stream.close(); - } catch (IOException ignored) {} + synchronized (streamLock) { + InputStream stream = inputStream; + if (stream == null) { + return; + } + inputStream = null; + try { + stream.close(); + } catch (IOException ignored) {} + } } private PasswordPromptFragment getPasswordPromptFragment() { From c5b480eaf4d1f2de46c1c3341ce95940c97b2889 Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:26:24 +0100 Subject: [PATCH 04/10] Move diskIO off main thread --- .../app/grapheneos/pdfviewer/PdfViewer.java | 29 +++++--------- .../pdfviewer/viewModel/PdfViewModel.kt | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 29d917236..c0db23d8b 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -41,7 +41,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -299,6 +298,13 @@ protected void onCreate(Bundle savedInstanceState) { } }); + viewModel.getSaveError().observe(this, error -> { + if (error) { + snackbar.setText(R.string.error_while_saving).show(); + viewModel.clearSaveError(); + } + }); + getSupportFragmentManager().setFragmentResultListener(OutlineFragment.RESULT_KEY, this, (requestKey, result) -> { final int newPage = result.getInt(OutlineFragment.PAGE_KEY, -1); @@ -865,24 +871,7 @@ private String getCurrentDocumentName() { return fileName.length() > 2 ? fileName : title; } - private void saveDocumentAs(final Uri uri) { - try (final InputStream input = getContentResolver().openInputStream(this.uri); - final OutputStream output = getContentResolver().openOutputStream(uri)) { - if (input == null || output == null) { - throw new FileNotFoundException(); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - input.transferTo(output); - } else { - final byte[] buffer = new byte[16384]; - int read; - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } - } catch (final IOException | IllegalArgumentException | IllegalStateException | - SecurityException e) { - snackbar.setText(R.string.error_while_saving).show(); - } + private void saveDocumentAs(final Uri saveUri) { + viewModel.saveDocumentAs(getContentResolver(), uri, saveUri); } } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt index fd8b05563..da673a830 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt @@ -1,13 +1,21 @@ package app.grapheneos.pdfviewer.viewModel +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import app.grapheneos.pdfviewer.outline.OutlineNode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.FileNotFoundException +import java.io.IOException class PdfViewModel : ViewModel() { @@ -32,6 +40,9 @@ class PdfViewModel : ViewModel() { // WebView to get outline. Lazily loaded, and will be cached until a different PDF is loaded. val outline: MutableLiveData = MutableLiveData(OutlineStatus.NotLoaded) + private val _saveError = MutableLiveData() + val saveError: LiveData get() = _saveError + private val scope = CoroutineScope(Dispatchers.IO) override fun onCleared() { @@ -90,4 +101,31 @@ class PdfViewModel : ViewModel() { outline.postValue(if (hasOutline) OutlineStatus.Available else OutlineStatus.NoOutline) } } + + fun clearSaveError() { + _saveError.value = false + } + + fun saveDocumentAs(contentResolver: ContentResolver, source: Uri, destination: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + contentResolver.openInputStream(source)?.use { input -> + contentResolver.openOutputStream(destination)?.use { output -> + input.copyTo(output) + } ?: throw FileNotFoundException() + } ?: throw FileNotFoundException() + } catch (e: Exception) { + coroutineContext.ensureActive() + when (e) { + is IOException, is IllegalArgumentException, + is IllegalStateException, is SecurityException -> { + withContext(Dispatchers.Main) { + _saveError.value = true + } + } + else -> throw e + } + } + } + } } From c84e55b03df97bba3ad9d81e78dd6a685390385e Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:20:33 +0100 Subject: [PATCH 05/10] Remove Loader from PdfViewer --- .../app/grapheneos/pdfviewer/PdfViewer.java | 79 ++++++------------- .../fragment/DocumentPropertiesFragment.kt | 46 +++++++---- .../DocumentPropertiesAsyncTaskLoader.java | 48 ----------- .../loader/DocumentPropertiesLoader.kt | 35 ++------ .../pdfviewer/loader/DocumentProperty.kt | 2 +- .../pdfviewer/viewModel/PdfViewModel.kt | 28 ++++++- 6 files changed, 88 insertions(+), 150 deletions(-) delete mode 100644 app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index c0db23d8b..5fbd95510 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -33,8 +33,6 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; import com.google.android.material.snackbar.Snackbar; @@ -44,18 +42,18 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.List; +import java.util.Map; import app.grapheneos.pdfviewer.databinding.PdfviewerBinding; import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment; import app.grapheneos.pdfviewer.fragment.JumpToPageFragment; import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment; import app.grapheneos.pdfviewer.ktx.ViewKt; -import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader; +import app.grapheneos.pdfviewer.loader.DocumentProperty; import app.grapheneos.pdfviewer.outline.OutlineFragment; import app.grapheneos.pdfviewer.viewModel.PdfViewModel; -public class PdfViewer extends AppCompatActivity implements LoaderManager.LoaderCallbacks> { +public class PdfViewer extends AppCompatActivity { private static final String TAG = "PdfViewer"; private static final String STATE_WEBVIEW_CRASHED = "webview_crashed"; @@ -64,7 +62,6 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader 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 = 133; private static final String CONTENT_SECURITY_POLICY = @@ -129,8 +126,8 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private volatile int documentOrientationDegrees; private int documentState; private volatile String encryptedDocumentPassword; - private volatile List documentProperties; - private InputStream inputStream; + private volatile InputStream inputStream; + private boolean documentPropertiesLoaded; private PdfviewerBinding binding; private TextView textView; @@ -147,7 +144,8 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader if (resultData != null) { uri = result.getData().getData(); page = 1; - documentProperties = null; + documentPropertiesLoaded = false; + viewModel.clearDocumentProperties(); encryptedDocumentPassword = ""; viewModel.clearOutline(); loadPdf(); @@ -232,13 +230,11 @@ public void setNumPages(int numPages) { @JavascriptInterface public void setDocumentProperties(final String properties) { - if (documentProperties != null) { - throw new SecurityException("mDocumentProperties not null"); + if (documentPropertiesLoaded) { + throw new SecurityException("setDocumentProperties already called"); } - - final Bundle args = new Bundle(); - args.putString(KEY_PROPERTIES, properties); - runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this)); + documentPropertiesLoaded = true; + runOnUiThread(() -> viewModel.loadDocumentProperties(properties, numPages, uri)); } @JavascriptInterface @@ -305,6 +301,11 @@ protected void onCreate(Bundle savedInstanceState) { } }); + viewModel.getDocumentProperties().observe(this, properties -> { + setToolbarTitleWithDocumentName(); + invalidateOptionsMenu(); + }); + getSupportFragmentManager().setFragmentResultListener(OutlineFragment.RESULT_KEY, this, (requestKey, result) -> { final int newPage = result.getInt(OutlineFragment.PAGE_KEY, -1); @@ -481,11 +482,6 @@ public void onZoomEnd() { textView.setTextSize(18); textView.setPadding(PADDING, 0, PADDING, 0); - // If loaders are not being initialized in onCreate(), the result will not be delivered - // after orientation change (See FragmentHostCallback), thus initialize the - // loader manager impl so that the result will be delivered. - LoaderManager.getInstance(this); - snackbar = Snackbar.make(binding.getRoot(), "", Snackbar.LENGTH_LONG); final Intent intent = getIntent(); @@ -606,26 +602,8 @@ private int getWebViewRelease() { return Integer.parseInt(webViewVersionName.substring(0, webViewVersionName.indexOf("."))); } - @NonNull - @Override - public Loader> onCreateLoader(int id, Bundle args) { - return new DocumentPropertiesAsyncTaskLoader(this, args.getString(KEY_PROPERTIES), numPages, uri); - } - - @Override - public void onLoadFinished(@NonNull Loader> loader, List data) { - documentProperties = data; - invalidateOptionsMenu(); - setToolbarTitleWithDocumentName(); - LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID); - } - - @Override - public void onLoaderReset(@NonNull Loader> loader) { - documentProperties = null; - } - private void loadPdf() { + documentPropertiesLoaded = false; documentState = 0; showSystemUi(); invalidateOptionsMenu(); @@ -776,7 +754,7 @@ public boolean onPrepareOptionsMenu(@NonNull Menu menu) { enableDisableMenuItem(menu.findItem(R.id.action_previous), page > 1); enableDisableMenuItem(menu.findItem(R.id.action_save_as), uri != null); enableDisableMenuItem(menu.findItem(R.id.action_view_document_properties), - documentProperties != null); + viewModel.getDocumentProperties().getValue() != null); menu.findItem(R.id.action_outline).setVisible(viewModel.hasOutline()); @@ -825,8 +803,8 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } else if (itemId == R.id.action_view_document_properties) { DocumentPropertiesFragment - .newInstance(documentProperties) - .show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG); + .newInstance() + .show(getSupportFragmentManager(), DocumentPropertiesFragment.TAG); return true; } else if (itemId == R.id.action_jump_to_page) { new JumpToPageFragment() @@ -857,18 +835,11 @@ private void saveDocument() { } private String getCurrentDocumentName() { - if (documentProperties == null || documentProperties.isEmpty()) return ""; - String fileName = ""; - String title = ""; - for (CharSequence property : documentProperties) { - if (property.toString().startsWith("File name:")) { - fileName = property.toString().replace("File name:", ""); - } - if (property.toString().startsWith("Title:-")) { - title = property.toString().replace("Title:-", ""); - } - } - return fileName.length() > 2 ? fileName : title; + Map properties = viewModel.getDocumentProperties().getValue(); + if (properties == null || properties.isEmpty()) return ""; + String fileName = properties.getOrDefault(DocumentProperty.FileName, ""); + String title = properties.getOrDefault(DocumentProperty.Title, ""); + return !(fileName != null && fileName.isEmpty()) ? fileName : title; } private void saveDocumentAs(final Uri saveUri) { diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt index 57bb87b88..2606f8d24 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt @@ -1,29 +1,53 @@ package app.grapheneos.pdfviewer.fragment import android.app.Dialog +import android.graphics.Typeface import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.StyleSpan import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels import app.grapheneos.pdfviewer.R +import app.grapheneos.pdfviewer.loader.DocumentProperty +import app.grapheneos.pdfviewer.viewModel.PdfViewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder class DocumentPropertiesFragment : DialogFragment() { - // TODO replace with nav args once the `PdfViewer` activity is converted to kotlin - private val documentProperties: List by lazy { - requireArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES)?.toList() ?: emptyList() + private val viewModel by activityViewModels() + + private fun formatProperties(properties: Map): List { + return properties.map { (property, value) -> + val name = getString(property.nameResource) + SpannableStringBuilder() + .append(name) + .append(":\n") + .append(value) + .apply { + setSpan( + StyleSpan(Typeface.BOLD), + 0, + name.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val properties = viewModel.documentProperties.value + return MaterialAlertDialogBuilder(requireActivity()) .setPositiveButton(android.R.string.ok, null).apply { - if (documentProperties.isNotEmpty()) { + if (!properties.isNullOrEmpty()) { setTitle(getString(R.string.action_view_document_properties)) setAdapter( ArrayAdapter( requireActivity(), android.R.layout.simple_list_item_1, - documentProperties + formatProperties(properties) ), null ) } else { @@ -36,18 +60,8 @@ class DocumentPropertiesFragment : DialogFragment() { companion object { const val TAG = "DocumentPropertiesFragment" - private const val KEY_DOCUMENT_PROPERTIES = "document_properties" @JvmStatic - fun newInstance(metaData: List): DocumentPropertiesFragment { - val fragment = DocumentPropertiesFragment() - val args = Bundle() - args.putCharSequenceArrayList( - KEY_DOCUMENT_PROPERTIES, - metaData as ArrayList - ) - fragment.arguments = args - return fragment - } + fun newInstance() = DocumentPropertiesFragment() } } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java deleted file mode 100644 index eec2157b9..000000000 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesAsyncTaskLoader.java +++ /dev/null @@ -1,48 +0,0 @@ -package app.grapheneos.pdfviewer.loader; - -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.Nullable; -import androidx.loader.content.AsyncTaskLoader; - -import java.util.List; - -public class DocumentPropertiesAsyncTaskLoader extends AsyncTaskLoader> { - - public static final String TAG = "DocumentPropertiesLoader"; - - public static final int ID = 1; - - private final String properties; - private final int numPages; - private final Uri uri; - - public DocumentPropertiesAsyncTaskLoader(Context context, String properties, int numPages, Uri uri) { - super(context); - - this.properties = properties; - this.numPages = numPages; - this.uri = uri; - } - - - @Override - protected void onStartLoading() { - forceLoad(); - } - - @Nullable - @Override - public List loadInBackground() { - - DocumentPropertiesLoader loader = new DocumentPropertiesLoader( - getContext(), - properties, - numPages, - uri - ); - - return loader.loadAsList(); - } -} diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt index 3a3736f94..b4d6c7e5a 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt @@ -1,13 +1,9 @@ package app.grapheneos.pdfviewer.loader import android.content.Context -import android.graphics.Typeface import android.net.Uri import android.provider.OpenableColumns -import android.text.SpannableStringBuilder -import android.text.Spanned import android.text.format.Formatter -import android.text.style.StyleSpan import android.util.Log import androidx.core.database.getLongOrNull import app.grapheneos.pdfviewer.R @@ -20,27 +16,11 @@ class DocumentPropertiesLoader( private val uri: Uri ) { - fun loadAsList(): List { - return load().map { item -> - val name = context.getString(item.key.nameResource) - val value = item.value - - SpannableStringBuilder() - .append(name) - .append(":\n") - .append(value) - .apply { - setSpan( - StyleSpan(Typeface.BOLD), - 0, - name.length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - } + companion object { + const val TAG = "DocumentPropertiesLoader" } - private fun load(): Map { + fun load(): Map { val result = mutableMapOf() result.addFileProperties() result.addPageSizeProperty() @@ -67,16 +47,13 @@ class DocumentPropertiesLoader( context.getString(R.string.document_properties_invalid_date), parseExceptionListener = { parseException, value -> Log.w( - DocumentPropertiesAsyncTaskLoader.TAG, + TAG, "${parseException.message} for $value at offset: ${parseException.errorOffset}" ) } ).convert() - } catch (e: JSONException) { - Log.w( - DocumentPropertiesAsyncTaskLoader.TAG, - "invalid properties" - ) + } catch (_: JSONException) { + Log.w(TAG, "invalid properties") emptyMap() } } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt index bee1873c3..08a9b74fb 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt @@ -17,7 +17,7 @@ const val DEFAULT_VALUE = "-" enum class DocumentProperty( val key: String = "", - @StringRes val nameResource: Int, + @param:StringRes val nameResource: Int, val isDate: Boolean = false ) { FileName(key = "", nameResource = R.string.file_name), diff --git a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt index da673a830..a095525df 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt @@ -1,11 +1,14 @@ package app.grapheneos.pdfviewer.viewModel +import android.app.Application import android.content.ContentResolver import android.net.Uri +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader +import app.grapheneos.pdfviewer.loader.DocumentProperty import app.grapheneos.pdfviewer.outline.OutlineNode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,7 +20,7 @@ import kotlinx.coroutines.withContext import java.io.FileNotFoundException import java.io.IOException -class PdfViewModel : ViewModel() { +class PdfViewModel(application: Application) : AndroidViewModel(application) { enum class PasswordStatus { MissingPassword, @@ -42,6 +45,9 @@ class PdfViewModel : ViewModel() { private val _saveError = MutableLiveData() val saveError: LiveData get() = _saveError + private val _documentProperties = MutableLiveData?>() + val documentProperties: LiveData?> get() = _documentProperties + private var documentPropertiesLoading = false private val scope = CoroutineScope(Dispatchers.IO) @@ -128,4 +134,22 @@ class PdfViewModel : ViewModel() { } } } + + fun loadDocumentProperties(properties: String, numPages: Int, uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + val loader = DocumentPropertiesLoader(getApplication(), properties, numPages, uri) + val result = loader.load() + withContext(Dispatchers.Main) { + if (documentPropertiesLoading) { + _documentProperties.value = result + } + } + } + documentPropertiesLoading = true + } + + fun clearDocumentProperties() { + _documentProperties.value = null + documentPropertiesLoading = false + } } From 4bc75d8d16589a4f3faa781655db70d7ff46289a Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:27:27 +0100 Subject: [PATCH 06/10] Remove SaveInstanceState and use ViewModel instead --- .../app/grapheneos/pdfviewer/PdfViewer.java | 158 ++++++++---------- .../pdfviewer/fragment/JumpToPageFragment.kt | 4 +- .../pdfviewer/viewModel/PdfViewModel.kt | 49 +++++- 3 files changed, 119 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 5fbd95510..ccaae5f9b 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -6,7 +6,6 @@ import android.content.res.ColorStateList; import android.graphics.Color; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Gravity; @@ -56,12 +55,6 @@ public class PdfViewer extends AppCompatActivity { private static final String TAG = "PdfViewer"; - private static final String STATE_WEBVIEW_CRASHED = "webview_crashed"; - private static final String STATE_URI = "uri"; - 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 int MIN_WEBVIEW_RELEASE = 133; private static final String CONTENT_SECURITY_POLICY = @@ -116,16 +109,9 @@ public class PdfViewer extends AppCompatActivity { private final Object streamLock = new Object(); - private boolean webViewCrashed; - private volatile Uri uri; - public volatile int page; - public volatile int numPages; - private volatile float zoomRatio = 1f; private volatile float zoomFocusX = 0f; private volatile float zoomFocusY = 0f; - private volatile int documentOrientationDegrees; private int documentState; - private volatile String encryptedDocumentPassword; private volatile InputStream inputStream; private boolean documentPropertiesLoaded; @@ -142,11 +128,11 @@ public class PdfViewer extends AppCompatActivity { if (result.getResultCode() != RESULT_OK) return; Intent resultData = result.getData(); if (resultData != null) { - uri = result.getData().getData(); - page = 1; + handleNewUri(resultData.getData()); + viewModel.setPage(1); documentPropertiesLoaded = false; viewModel.clearDocumentProperties(); - encryptedDocumentPassword = ""; + viewModel.setEncryptedDocumentPassword(""); viewModel.clearOutline(); loadPdf(); invalidateOptionsMenu(); @@ -169,27 +155,29 @@ public class PdfViewer extends AppCompatActivity { private class Channel { @JavascriptInterface public void setHasDocumentOutline(final boolean hasOutline) { - viewModel.setHasOutline(hasOutline); + runOnUiThread(() -> viewModel.setHasOutline(hasOutline)); } @JavascriptInterface public void setDocumentOutline(final String outline) { - viewModel.parseOutlineString(outline); + runOnUiThread(() -> viewModel.parseOutlineString(outline)); } @JavascriptInterface public int getPage() { - return page; + return viewModel.getPage(); } @JavascriptInterface public float getZoomRatio() { - return zoomRatio; + return viewModel.getZoomRatio(); } @JavascriptInterface public void setZoomRatio(final float ratio) { - zoomRatio = Math.max(Math.min(ratio, MAX_ZOOM_RATIO), MIN_ZOOM_RATIO); + runOnUiThread(() -> + viewModel.setZoomRatio(Math.max(Math.min(ratio, MAX_ZOOM_RATIO), MIN_ZOOM_RATIO)) + ); } @JavascriptInterface @@ -219,12 +207,12 @@ public float getMaxZoomRatio() { @JavascriptInterface public int getDocumentOrientationDegrees() { - return documentOrientationDegrees; + return viewModel.getDocumentOrientationDegrees(); } @JavascriptInterface public void setNumPages(int numPages) { - PdfViewer.this.numPages = numPages; + viewModel.setNumPages(numPages); runOnUiThread(PdfViewer.this::invalidateOptionsMenu); } @@ -234,13 +222,15 @@ public void setDocumentProperties(final String properties) { throw new SecurityException("setDocumentProperties already called"); } documentPropertiesLoaded = true; + final int numPages = viewModel.getNumPages(); + final Uri uri = viewModel.getUri(); runOnUiThread(() -> viewModel.loadDocumentProperties(properties, numPages, uri)); } @JavascriptInterface public void showPasswordPrompt() { runOnUiThread(() -> { - if (!getPasswordPromptFragment().isAdded()){ + if (!getPasswordPromptFragment().isAdded()) { getPasswordPromptFragment().show(getSupportFragmentManager(), PasswordPromptFragment.class.getName()); } }); @@ -264,7 +254,7 @@ public void onLoaded() { @JavascriptInterface public String getPassword() { - return encryptedDocumentPassword != null ? encryptedDocumentPassword : ""; + return viewModel.getEncryptedDocumentPassword(); } } @@ -285,7 +275,7 @@ protected void onCreate(Bundle savedInstanceState) { binding = PdfviewerBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - viewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(PdfViewModel.class); + viewModel = new ViewModelProvider(this).get(PdfViewModel.class); viewModel.getOutline().observe(this, requested -> { if (requested instanceof PdfViewModel.OutlineStatus.Requested) { @@ -349,6 +339,8 @@ private WebResourceResponse fromAsset(final String mime, final String path) { @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + if (viewModel.getUri() == null) return null; + if (!"GET".equals(request.getMethod())) { return null; } @@ -365,7 +357,7 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque synchronized (streamLock) { maybeCloseInputStream(); try { - inputStream = getContentResolver().openInputStream(uri); + inputStream = getContentResolver().openInputStream(viewModel.getUri()); if (inputStream == null) { throw new FileNotFoundException(); } @@ -430,13 +422,13 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request public void onPageFinished(WebView view, String url) { documentState = STATE_LOADED; invalidateOptionsMenu(); - loadPdfWithPassword(encryptedDocumentPassword); + loadPdfWithPassword(viewModel.getEncryptedDocumentPassword()); } @Override public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { if (detail.didCrash()) { - webViewCrashed = true; + viewModel.setWebViewCrashed(true); showWebViewCrashed(); invalidateOptionsMenu(); purgeWebView(); @@ -450,10 +442,10 @@ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) new GestureHelper.GestureListener() { @Override public boolean onTapUp() { - if (uri != null) { + if (viewModel.getUri() != null) { binding.webview.evaluateJavascript("isTextSelected()", selection -> { if (!Boolean.parseBoolean(selection)) { - if (getSupportActionBar().isShowing()) { + if (getSupportActionBar() != null && getSupportActionBar().isShowing()) { hideSystemUi(); } else { showSystemUi(); @@ -485,7 +477,7 @@ public void onZoomEnd() { snackbar = Snackbar.make(binding.getRoot(), "", Snackbar.LENGTH_LONG); final Intent intent = getIntent(); - if (Intent.ACTION_VIEW.equals(intent.getAction())) { + if (savedInstanceState == null && Intent.ACTION_VIEW.equals(intent.getAction())) { final String type = intent.getType(); if (!"application/pdf".equals(type) && type != null) { snackbar.setText(R.string.invalid_mime_type).show(); @@ -494,38 +486,22 @@ public void onZoomEnd() { if (type == null) { Log.w(TAG, "MIME type is null, but we'll try to load it anyway"); } - uri = intent.getData(); - page = 1; - } - - if (savedInstanceState != null) { - webViewCrashed = savedInstanceState.getBoolean(STATE_WEBVIEW_CRASHED); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - uri = savedInstanceState.getParcelable(STATE_URI, Uri.class); - } else { - @SuppressWarnings("deprecation") - final Uri uri = savedInstanceState.getParcelable(STATE_URI); - this.uri = uri; - } - page = savedInstanceState.getInt(STATE_PAGE); - zoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO); - documentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES); - encryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD); + handleNewUri(intent.getData()); + viewModel.setPage(1); } binding.webviewAlertReload.setOnClickListener(v -> { - webViewCrashed = false; + viewModel.setWebViewCrashed(false); recreate(); }); - if (webViewCrashed) { + if (viewModel.getWebViewCrashed()) { showWebViewCrashed(); - } else if (uri != null) { - if ("file".equals(uri.getScheme())) { + } else if (viewModel.getUri() != null) { + if ("file".equals(viewModel.getUri().getScheme())) { snackbar.setText(R.string.legacy_file_uri).show(); return; } - loadPdf(); } } @@ -581,7 +557,7 @@ private void setToolbarTitleWithDocumentName() { protected void onResume() { super.onResume(); - if (!webViewCrashed) { + if (!viewModel.getWebViewCrashed()) { // The user could have left the activity to update the WebView invalidateOptionsMenu(); if (getWebViewRelease() >= MIN_WEBVIEW_RELEASE) { @@ -611,7 +587,7 @@ private void loadPdf() { } public void loadPdfWithPassword(final String password) { - encryptedDocumentPassword = password; + viewModel.setEncryptedDocumentPassword(password); binding.webview.evaluateJavascript("loadDocument()", null); } @@ -620,10 +596,11 @@ private void renderPage(final int zoom) { } private void documentOrientationChanged(final int orientationDegreesOffset) { - documentOrientationDegrees = (documentOrientationDegrees + orientationDegreesOffset) % 360; - if (documentOrientationDegrees < 0) { - documentOrientationDegrees += 360; + int degrees = (viewModel.getDocumentOrientationDegrees() + orientationDegreesOffset) % 360; + if (degrees < 0) { + degrees += 360; } + viewModel.setDocumentOrientationDegrees(degrees); renderPage(0); } @@ -635,10 +612,10 @@ private void openDocument() { } private void shareDocument() { - if (uri != null) { + if (viewModel.getUri() != null) { Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setDataAndTypeAndNormalize(uri, "application/pdf"); - shareIntent.putExtra(Intent.EXTRA_STREAM, uri); + shareIntent.setDataAndTypeAndNormalize(viewModel.getUri(), "application/pdf"); + shareIntent.putExtra(Intent.EXTRA_STREAM, viewModel.getUri()); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, getString(R.string.action_share))); } else { @@ -647,7 +624,7 @@ private void shareDocument() { } private void zoom(float scaleFactor, float focusX, float focusY, boolean end) { - zoomRatio = Math.min(Math.max(zoomRatio * scaleFactor, MIN_ZOOM_RATIO), MAX_ZOOM_RATIO); + viewModel.setZoomRatio(Math.min(Math.max(viewModel.getZoomRatio() * scaleFactor, MIN_ZOOM_RATIO), MAX_ZOOM_RATIO)); zoomFocusX = focusX; zoomFocusY = focusY; renderPage(end ? 1 : 2); @@ -666,8 +643,8 @@ private static void enableDisableMenuItem(MenuItem item, boolean enable) { } public void onJumpToPageInDocument(final int selected_page) { - if (selected_page >= 1 && selected_page <= numPages && page != selected_page) { - page = selected_page; + if (selected_page >= 1 && selected_page <= viewModel.getNumPages() && viewModel.getPage() != selected_page) { + viewModel.setPage(selected_page); renderPage(0); showPageNumber(); invalidateOptionsMenu(); @@ -684,22 +661,11 @@ private void hideSystemUi() { getSupportActionBar().hide(); } - @Override - public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { - super.onSaveInstanceState(savedInstanceState); - savedInstanceState.putBoolean(STATE_WEBVIEW_CRASHED, webViewCrashed); - savedInstanceState.putParcelable(STATE_URI, uri); - savedInstanceState.putInt(STATE_PAGE, page); - savedInstanceState.putFloat(STATE_ZOOM_RATIO, zoomRatio); - savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, documentOrientationDegrees); - savedInstanceState.putString(STATE_ENCRYPTED_DOCUMENT_PASSWORD, encryptedDocumentPassword); - } - private void showPageNumber() { if (toast != null) { toast.cancel(); } - textView.setText(String.format("%s/%s", page, numPages)); + textView.setText(String.format("%s/%s", viewModel.getPage(), viewModel.getNumPages())); toast = new Toast(this); toast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING); toast.setDuration(Toast.LENGTH_SHORT); @@ -748,17 +714,17 @@ public boolean onPrepareOptionsMenu(@NonNull Menu menu) { enableDisableMenuItem(menu.findItem(R.id.action_open), - !webViewCrashed && getWebViewRelease() >= MIN_WEBVIEW_RELEASE); - enableDisableMenuItem(menu.findItem(R.id.action_share), uri != null); - enableDisableMenuItem(menu.findItem(R.id.action_next), page < numPages); - enableDisableMenuItem(menu.findItem(R.id.action_previous), page > 1); - enableDisableMenuItem(menu.findItem(R.id.action_save_as), uri != null); + !viewModel.getWebViewCrashed() && getWebViewRelease() >= MIN_WEBVIEW_RELEASE); + enableDisableMenuItem(menu.findItem(R.id.action_share), viewModel.getUri() != null); + enableDisableMenuItem(menu.findItem(R.id.action_next), viewModel.getPage() < viewModel.getNumPages()); + enableDisableMenuItem(menu.findItem(R.id.action_previous), viewModel.getPage() > 1); + enableDisableMenuItem(menu.findItem(R.id.action_save_as), viewModel.getUri() != null); enableDisableMenuItem(menu.findItem(R.id.action_view_document_properties), viewModel.getDocumentProperties().getValue() != null); menu.findItem(R.id.action_outline).setVisible(viewModel.hasOutline()); - if (webViewCrashed) { + if (viewModel.getWebViewCrashed()) { for (final int id : ids) { enableDisableMenuItem(menu.findItem(id), false); } @@ -771,16 +737,16 @@ public boolean onPrepareOptionsMenu(@NonNull Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.action_previous) { - onJumpToPageInDocument(page - 1); + onJumpToPageInDocument(viewModel.getPage() - 1); return true; } else if (itemId == R.id.action_next) { - onJumpToPageInDocument(page + 1); + onJumpToPageInDocument(viewModel.getPage() + 1); return true; } else if (itemId == R.id.action_first) { onJumpToPageInDocument(1); return true; } else if (itemId == R.id.action_last) { - onJumpToPageInDocument(numPages); + onJumpToPageInDocument(viewModel.getNumPages()); return true; } else if (itemId == R.id.action_open) { openDocument(); @@ -793,7 +759,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } else if (itemId == R.id.action_outline) { OutlineFragment outlineFragment = - OutlineFragment.newInstance(page, getCurrentDocumentName()); + OutlineFragment.newInstance(viewModel.getPage(), getCurrentDocumentName()); getSupportFragmentManager().beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) // fullscreen fragment, since content root view == activity's root view @@ -843,6 +809,20 @@ private String getCurrentDocumentName() { } private void saveDocumentAs(final Uri saveUri) { - viewModel.saveDocumentAs(getContentResolver(), uri, saveUri); + if (viewModel.getUri() == null) return; + viewModel.saveDocumentAs(getContentResolver(), viewModel.getUri(), saveUri); + } + + private void handleNewUri(Uri newUri) { + Uri oldUri = viewModel.getUri(); + if (oldUri != null) { + try { + getContentResolver().releasePersistableUriPermission(oldUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (SecurityException ignored) {} + } + try { + getContentResolver().takePersistableUriPermission(newUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (SecurityException ignored) {} + viewModel.setUri(newUri); } } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt index 581d3bc76..432974775 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/JumpToPageFragment.kt @@ -31,8 +31,8 @@ class JumpToPageFragment : DialogFragment() { picker.value = savedInstanceState.getInt(STATE_PICKER_CUR) } else { picker.minValue = 1 - picker.maxValue = viewerActivity.numPages - picker.value = viewerActivity.page + picker.maxValue = viewerActivity.viewModel.numPages + picker.value = viewerActivity.viewModel.page } val layout = FrameLayout(requireActivity()) layout.addView( diff --git a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt index a095525df..f14e2635e 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt @@ -6,6 +6,7 @@ import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader import app.grapheneos.pdfviewer.loader.DocumentProperty @@ -20,7 +21,53 @@ import kotlinx.coroutines.withContext import java.io.FileNotFoundException import java.io.IOException -class PdfViewModel(application: Application) : AndroidViewModel(application) { +class PdfViewModel( + application: Application, + private val savedStateHandle: SavedStateHandle +) : AndroidViewModel(application) { + + companion object { + private const val STATE_URI: String = "uri" + private const val STATE_PAGE: String = "page" + private const val STATE_ZOOM_RATIO: String = "zoomRatio" + private const val STATE_DOCUMENT_ORIENTATION_DEGREES: String = "documentOrientationDegrees" + } + + @Volatile + var uri: Uri? = savedStateHandle[STATE_URI] + set(value) { + field = value + savedStateHandle[STATE_URI] = value + } + + @Volatile + var page: Int = savedStateHandle[STATE_PAGE] ?: 1 + set(value) { + field = value + savedStateHandle[STATE_PAGE] = value + } + + @Volatile + var zoomRatio: Float = savedStateHandle[STATE_ZOOM_RATIO] ?: 1f + set(value) { + field = value + savedStateHandle[STATE_ZOOM_RATIO] = value + } + + @Volatile + var documentOrientationDegrees: Int = savedStateHandle[STATE_DOCUMENT_ORIENTATION_DEGREES] ?: 0 + set(value) { + field = value + savedStateHandle[STATE_DOCUMENT_ORIENTATION_DEGREES] = value + } + + @Volatile + var numPages: Int = 0 + + @Volatile + var encryptedDocumentPassword: String = "" + + var webViewCrashed: Boolean = false enum class PasswordStatus { MissingPassword, From d034dc08516d4d0ad5cf839ed95495ed0783b543 Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:52:57 +0100 Subject: [PATCH 07/10] Keep zoom ratio during rotation as intended --- app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java | 2 ++ .../java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt | 2 +- viewer/js/index.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index ccaae5f9b..6e0bccfe5 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -134,6 +134,7 @@ public class PdfViewer extends AppCompatActivity { viewModel.clearDocumentProperties(); viewModel.setEncryptedDocumentPassword(""); viewModel.clearOutline(); + viewModel.setZoomRatio(0f); loadPdf(); invalidateOptionsMenu(); } @@ -487,6 +488,7 @@ public void onZoomEnd() { Log.w(TAG, "MIME type is null, but we'll try to load it anyway"); } handleNewUri(intent.getData()); + viewModel.setZoomRatio(0f); viewModel.setPage(1); } diff --git a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt index f14e2635e..9d33c3a40 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt @@ -48,7 +48,7 @@ class PdfViewModel( } @Volatile - var zoomRatio: Float = savedStateHandle[STATE_ZOOM_RATIO] ?: 1f + var zoomRatio: Float = savedStateHandle[STATE_ZOOM_RATIO] ?: 0f set(value) { field = value savedStateHandle[STATE_ZOOM_RATIO] = value diff --git a/viewer/js/index.js b/viewer/js/index.js index 3eed8feec..39dfff7be 100644 --- a/viewer/js/index.js +++ b/viewer/js/index.js @@ -215,7 +215,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { const defaultZoomRatio = getDefaultZoomRatio(page, orientationDegrees); - if (cache.length === 0) { + if (newZoomRatio === 0) { zoomRatio = defaultZoomRatio; newZoomRatio = defaultZoomRatio; channel.setZoomRatio(defaultZoomRatio); From ba3ee8ab300caf33bef1c6a47c2c551197cba5fa Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:34:25 +0100 Subject: [PATCH 08/10] Simplify menu handling --- .../app/grapheneos/pdfviewer/PdfViewer.java | 85 ++++++++----------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 6e0bccfe5..a7c342ec9 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -103,15 +103,13 @@ public class PdfViewer extends AppCompatActivity { private static final int MAX_RENDER_PIXELS = 1 << 23; // 8 mega-pixels private static final int ALPHA_LOW = 130; private static final int ALPHA_HIGH = 255; - private static final int STATE_LOADED = 1; - private static final int STATE_END = 2; private static final int PADDING = 10; private final Object streamLock = new Object(); private volatile float zoomFocusX = 0f; private volatile float zoomFocusY = 0f; - private int documentState; + private boolean documentLoaded; private volatile InputStream inputStream; private boolean documentPropertiesLoaded; @@ -421,7 +419,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request @Override public void onPageFinished(WebView view, String url) { - documentState = STATE_LOADED; + documentLoaded = true; invalidateOptionsMenu(); loadPdfWithPassword(viewModel.getEncryptedDocumentPassword()); } @@ -582,7 +580,7 @@ private int getWebViewRelease() { private void loadPdf() { documentPropertiesLoaded = false; - documentState = 0; + documentLoaded = false; showSystemUi(); invalidateOptionsMenu(); binding.webview.loadUrl("https://localhost/viewer/index.html"); @@ -637,10 +635,11 @@ private void zoomEnd() { renderPage(1); } - private static void enableDisableMenuItem(MenuItem item, boolean enable) { - item.setEnabled(enable); + private static void setMenuItemState(MenuItem item, boolean visible, boolean enabled) { + item.setVisible(visible); + item.setEnabled(enabled); if (item.getIcon() != null) { - item.getIcon().setAlpha(enable ? ALPHA_HIGH : ALPHA_LOW); + item.getIcon().setAlpha(enabled ? ALPHA_HIGH : ALPHA_LOW); } } @@ -688,48 +687,35 @@ public boolean onCreateOptionsMenu(@NonNull Menu menu) { @Override public boolean onPrepareOptionsMenu(@NonNull Menu menu) { - final ArrayList ids = new ArrayList<>(Arrays.asList(R.id.action_jump_to_page, - R.id.action_next, R.id.action_previous, R.id.action_first, R.id.action_last, - R.id.action_rotate_clockwise, R.id.action_rotate_counterclockwise, - R.id.action_view_document_properties, R.id.action_share, R.id.action_save_as, - R.id.action_outline)); - if (BuildConfig.DEBUG) { - ids.add(R.id.debug_action_toggle_text_layer_visibility); - ids.add(R.id.debug_action_crash_webview); - } - if (documentState < STATE_LOADED) { - for (final int id : ids) { - final MenuItem item = menu.findItem(id); - if (item.isVisible()) { - item.setVisible(false); - } - } - } else if (documentState == STATE_LOADED) { - for (final int id : ids) { - final MenuItem item = menu.findItem(id); - if (!item.isVisible()) { - item.setVisible(true); - } - } - documentState = STATE_END; - } - + final boolean loaded = documentLoaded; + final boolean crashed = viewModel.getWebViewCrashed(); + final boolean enabled = loaded && !crashed; + + setMenuItemState(menu.findItem(R.id.action_open), true, + !crashed && getWebViewRelease() >= MIN_WEBVIEW_RELEASE); + setMenuItemState(menu.findItem(R.id.action_jump_to_page), loaded, enabled); + setMenuItemState(menu.findItem(R.id.action_next), loaded, + enabled && viewModel.getPage() < viewModel.getNumPages()); + setMenuItemState(menu.findItem(R.id.action_previous), loaded, + enabled && viewModel.getPage() > 1); + setMenuItemState(menu.findItem(R.id.action_first), loaded, enabled); + setMenuItemState(menu.findItem(R.id.action_last), loaded, enabled); + setMenuItemState(menu.findItem(R.id.action_rotate_clockwise), loaded, enabled); + setMenuItemState(menu.findItem(R.id.action_rotate_counterclockwise), loaded, enabled); + setMenuItemState(menu.findItem(R.id.action_view_document_properties), loaded, + enabled && viewModel.getDocumentProperties().getValue() != null); + setMenuItemState(menu.findItem(R.id.action_share), loaded, + enabled && viewModel.getUri() != null); + setMenuItemState(menu.findItem(R.id.action_save_as), loaded, + enabled && viewModel.getUri() != null); + setMenuItemState(menu.findItem(R.id.action_outline), + loaded && viewModel.hasOutline(), enabled); - enableDisableMenuItem(menu.findItem(R.id.action_open), - !viewModel.getWebViewCrashed() && getWebViewRelease() >= MIN_WEBVIEW_RELEASE); - enableDisableMenuItem(menu.findItem(R.id.action_share), viewModel.getUri() != null); - enableDisableMenuItem(menu.findItem(R.id.action_next), viewModel.getPage() < viewModel.getNumPages()); - enableDisableMenuItem(menu.findItem(R.id.action_previous), viewModel.getPage() > 1); - enableDisableMenuItem(menu.findItem(R.id.action_save_as), viewModel.getUri() != null); - enableDisableMenuItem(menu.findItem(R.id.action_view_document_properties), - viewModel.getDocumentProperties().getValue() != null); - - menu.findItem(R.id.action_outline).setVisible(viewModel.hasOutline()); - - if (viewModel.getWebViewCrashed()) { - for (final int id : ids) { - enableDisableMenuItem(menu.findItem(id), false); - } + if (BuildConfig.DEBUG) { + setMenuItemState(menu.findItem(R.id.debug_action_toggle_text_layer_visibility), + loaded, enabled); + setMenuItemState(menu.findItem(R.id.debug_action_crash_webview), + loaded, enabled); } return true; @@ -783,6 +769,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } else if (itemId == R.id.action_save_as) { saveDocument(); + return true; } else if (itemId == R.id.debug_action_toggle_text_layer_visibility) { binding.webview.evaluateJavascript("toggleTextLayerVisibility()", null); return true; From b2b63d501e77fed23c2ada4079be1b996d469d94 Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:43:10 +0100 Subject: [PATCH 09/10] Remove toast and use snackbar --- .../app/grapheneos/pdfviewer/PdfViewer.java | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index a7c342ec9..f1b2c2c08 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -3,12 +3,10 @@ import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageInfo; -import android.content.res.ColorStateList; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.util.Log; -import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -21,8 +19,6 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; -import android.widget.TextView; -import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -38,8 +34,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -103,7 +97,6 @@ public class PdfViewer extends AppCompatActivity { private static final int MAX_RENDER_PIXELS = 1 << 23; // 8 mega-pixels private static final int ALPHA_LOW = 130; private static final int ALPHA_HIGH = 255; - private static final int PADDING = 10; private final Object streamLock = new Object(); @@ -111,11 +104,9 @@ public class PdfViewer extends AppCompatActivity { private volatile float zoomFocusY = 0f; private boolean documentLoaded; private volatile InputStream inputStream; - private boolean documentPropertiesLoaded; + private volatile boolean documentPropertiesLoaded; private PdfviewerBinding binding; - private TextView textView; - private Toast toast; private Snackbar snackbar; private PasswordPromptFragment passwordPromptFragment; public PdfViewModel viewModel; @@ -467,12 +458,6 @@ public void onZoomEnd() { } }); - textView = new TextView(this); - textView.setBackgroundColor(Color.DKGRAY); - textView.setTextColor(ColorStateList.valueOf(Color.WHITE)); - textView.setTextSize(18); - textView.setPadding(PADDING, 0, PADDING, 0); - snackbar = Snackbar.make(binding.getRoot(), "", Snackbar.LENGTH_LONG); final Intent intent = getIntent(); @@ -663,15 +648,9 @@ private void hideSystemUi() { } private void showPageNumber() { - if (toast != null) { - toast.cancel(); - } - textView.setText(String.format("%s/%s", viewModel.getPage(), viewModel.getNumPages())); - toast = new Toast(this); - toast.setGravity(Gravity.BOTTOM | Gravity.END, PADDING, PADDING); - toast.setDuration(Toast.LENGTH_SHORT); - toast.setView(textView); - toast.show(); + Snackbar.make(binding.webview, + String.format("%s/%s", viewModel.getPage(), viewModel.getNumPages()), + Snackbar.LENGTH_SHORT).show(); } @Override From 6c86f85586832b4e3eae1c27e2fc54df1c382b37 Mon Sep 17 00:00:00 2001 From: ggtlvkma356 <265052551+ggtlvkma356@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:21:54 +0200 Subject: [PATCH 10/10] Rename DocumentPropertiesLoader and relevant functions --- .../main/java/app/grapheneos/pdfviewer/PdfViewer.java | 6 ++++-- .../pdfviewer/fragment/DocumentPropertiesFragment.kt | 2 +- .../DocumentPropertiesRetriever.kt} | 8 ++++---- .../{loader => properties}/DocumentProperty.kt | 2 +- .../PDFJsPropertiesToDocumentPropertyConverter.kt | 2 +- .../app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt | 10 +++++----- 6 files changed, 16 insertions(+), 14 deletions(-) rename app/src/main/java/app/grapheneos/pdfviewer/{loader/DocumentPropertiesLoader.kt => properties/DocumentPropertiesRetriever.kt} (93%) rename app/src/main/java/app/grapheneos/pdfviewer/{loader => properties}/DocumentProperty.kt (97%) rename app/src/main/java/app/grapheneos/pdfviewer/{loader => properties}/PDFJsPropertiesToDocumentPropertyConverter.kt (97%) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index f1b2c2c08..ce400067c 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -42,7 +42,7 @@ import app.grapheneos.pdfviewer.fragment.JumpToPageFragment; import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment; import app.grapheneos.pdfviewer.ktx.ViewKt; -import app.grapheneos.pdfviewer.loader.DocumentProperty; +import app.grapheneos.pdfviewer.properties.DocumentProperty; import app.grapheneos.pdfviewer.outline.OutlineFragment; import app.grapheneos.pdfviewer.viewModel.PdfViewModel; @@ -214,7 +214,9 @@ public void setDocumentProperties(final String properties) { documentPropertiesLoaded = true; final int numPages = viewModel.getNumPages(); final Uri uri = viewModel.getUri(); - runOnUiThread(() -> viewModel.loadDocumentProperties(properties, numPages, uri)); + if (uri != null) { + runOnUiThread(() -> viewModel.retrieveDocumentProperties(properties, numPages, uri)); + } } @JavascriptInterface diff --git a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt index 2606f8d24..1d71b23b4 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/fragment/DocumentPropertiesFragment.kt @@ -10,7 +10,7 @@ import android.widget.ArrayAdapter import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import app.grapheneos.pdfviewer.R -import app.grapheneos.pdfviewer.loader.DocumentProperty +import app.grapheneos.pdfviewer.properties.DocumentProperty import app.grapheneos.pdfviewer.viewModel.PdfViewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt b/app/src/main/java/app/grapheneos/pdfviewer/properties/DocumentPropertiesRetriever.kt similarity index 93% rename from app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt rename to app/src/main/java/app/grapheneos/pdfviewer/properties/DocumentPropertiesRetriever.kt index b4d6c7e5a..7de261350 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentPropertiesLoader.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/properties/DocumentPropertiesRetriever.kt @@ -1,4 +1,4 @@ -package app.grapheneos.pdfviewer.loader +package app.grapheneos.pdfviewer.properties import android.content.Context import android.net.Uri @@ -9,7 +9,7 @@ import androidx.core.database.getLongOrNull import app.grapheneos.pdfviewer.R import org.json.JSONException -class DocumentPropertiesLoader( +class DocumentPropertiesRetriever( private val context: Context, private val properties: String, private val numPages: Int, @@ -17,10 +17,10 @@ class DocumentPropertiesLoader( ) { companion object { - const val TAG = "DocumentPropertiesLoader" + const val TAG = "DocumentPropertiesRetriever" } - fun load(): Map { + fun retrieve(): Map { val result = mutableMapOf() result.addFileProperties() result.addPageSizeProperty() diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt b/app/src/main/java/app/grapheneos/pdfviewer/properties/DocumentProperty.kt similarity index 97% rename from app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt rename to app/src/main/java/app/grapheneos/pdfviewer/properties/DocumentProperty.kt index 08a9b74fb..e84f28035 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/DocumentProperty.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/properties/DocumentProperty.kt @@ -1,4 +1,4 @@ -package app.grapheneos.pdfviewer.loader +package app.grapheneos.pdfviewer.properties import androidx.annotation.StringRes import app.grapheneos.pdfviewer.R diff --git a/app/src/main/java/app/grapheneos/pdfviewer/loader/PDFJsPropertiesToDocumentPropertyConverter.kt b/app/src/main/java/app/grapheneos/pdfviewer/properties/PDFJsPropertiesToDocumentPropertyConverter.kt similarity index 97% rename from app/src/main/java/app/grapheneos/pdfviewer/loader/PDFJsPropertiesToDocumentPropertyConverter.kt rename to app/src/main/java/app/grapheneos/pdfviewer/properties/PDFJsPropertiesToDocumentPropertyConverter.kt index 3bf8137cf..3df2805cb 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/loader/PDFJsPropertiesToDocumentPropertyConverter.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/properties/PDFJsPropertiesToDocumentPropertyConverter.kt @@ -1,4 +1,4 @@ -package app.grapheneos.pdfviewer.loader +package app.grapheneos.pdfviewer.properties import app.grapheneos.pdfviewer.Utils import org.json.JSONException diff --git a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt index 9d33c3a40..698445fba 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt +++ b/app/src/main/java/app/grapheneos/pdfviewer/viewModel/PdfViewModel.kt @@ -8,8 +8,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader -import app.grapheneos.pdfviewer.loader.DocumentProperty +import app.grapheneos.pdfviewer.properties.DocumentPropertiesRetriever +import app.grapheneos.pdfviewer.properties.DocumentProperty import app.grapheneos.pdfviewer.outline.OutlineNode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -182,10 +182,10 @@ class PdfViewModel( } } - fun loadDocumentProperties(properties: String, numPages: Int, uri: Uri) { + fun retrieveDocumentProperties(properties: String, numPages: Int, uri: Uri) { viewModelScope.launch(Dispatchers.IO) { - val loader = DocumentPropertiesLoader(getApplication(), properties, numPages, uri) - val result = loader.load() + val loader = DocumentPropertiesRetriever(getApplication(), properties, numPages, uri) + val result = loader.retrieve() withContext(Dispatchers.Main) { if (documentPropertiesLoading) { _documentProperties.value = result