Skip to content

Multiple improvements and refactors#616

Open
ggtlvkma356 wants to merge 10 commits intoGrapheneOS:mainfrom
ggtlvkma356:pdfviewer-improvements
Open

Multiple improvements and refactors#616
ggtlvkma356 wants to merge 10 commits intoGrapheneOS:mainfrom
ggtlvkma356:pdfviewer-improvements

Conversation

@ggtlvkma356
Copy link
Copy Markdown

List of changes

  1. Move UI operations to the main thread.

  2. Make shared mutable state thread-safe.

  3. Move file saving off UI thread.

  4. Remove Loader. Document properties are loaded using ViewModel.

  5. Remove onSaveInstanceState. Persistent states are managed with ViewModel.

  6. Simplify Menu handling.

  7. Display page number with Snackbar instead of Toast.

  8. Remove Hungarian notation.

Copy link
Copy Markdown
Member

@inthewaves inthewaves left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general note: Technically, JavaScript is single-threaded and the JS Bridge code blocks when calling bridge methods, so concurrency between bridge calls is less of an issue (i.e., JS will never concurrently call bridge methods). Of course, the JS bridge background thread in Java still can have concurrency issue with pure Android threads

From Chromium source on android_webview/docs/java-bridge.md: "Calls to interface methods are synchronous—JavaScript VM stops and waits for a result to be returned from the invoked method. In Chromium, this means that the IPC message sent from a renderer to the browser must be synchronous."

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;
Copy link
Copy Markdown
Member

@inthewaves inthewaves Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanding it right now would be fileName == null || !fileName.isEmpty() so it will accept null if that's intentional. (although null never happens because of properties.getOrDefault

We can consider also just using !TextUtils.isEmpty. TextUtils.isEmpty is a framework method that returns true iff the string is null or 0-length

Comment on lines +185 to +195
fun retrieveDocumentProperties(properties: String, numPages: Int, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
val loader = DocumentPropertiesRetriever(getApplication(), properties, numPages, uri)
val result = loader.retrieve()
withContext(Dispatchers.Main) {
if (documentPropertiesLoading) {
_documentProperties.value = result
}
}
}
documentPropertiesLoading = true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think documentPropertiesLoading = true should be before viewModelScope.launch(Dispatchers.IO). Technically doesn't matter since this seems to be on main thread anyway, so withContext(Dispatchers.Main) would always catch this

private volatile float zoomFocusY = 0f;
private boolean documentLoaded;
private volatile InputStream inputStream;
private volatile boolean documentPropertiesLoaded;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think volatile works here for this pattern. For this compound operation of read-and-then-set, it only helps with read visibility

@JavascriptInterface
public void setDocumentProperties(final String properties) {
    if (documentPropertiesLoaded) {
        throw new SecurityException("setDocumentProperties already called");
    }
    documentPropertiesLoaded = true;
    ...
}

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible.

AtomicBoolean#compareAndSet would be more correct here.

Just as a note from my own looking: even if this bridge method itself is synchronous with respect to JS code, documentPropertiesLoaded is still technically read/written by Android main thread in loadPdf(), so doesn't seem correct to drop this pattern entirely

Comment on lines 751 to +753
} else if (itemId == R.id.action_save_as) {
saveDocument();
return true;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants