Multiple improvements and refactors#616
Conversation
inthewaves
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
| } else if (itemId == R.id.action_save_as) { | ||
| saveDocument(); | ||
| return true; |
6c86f85 to
ab36735
Compare
5aa4a3c to
0984f3c
Compare
| viewModelScope.launch(Dispatchers.IO) { | ||
| val loader = DocumentPropertiesRetriever(getApplication(), properties, numPages, uri) | ||
| val result = loader.retrieve() | ||
| val name = resolveDocumentName(result) | ||
| withContext(Dispatchers.Main) { | ||
| if (documentPropertiesLoading) { | ||
| _documentProperties.value = result | ||
| _documentName.value = name | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
There's technically a race here if document property loading takes a long time. If you load properties for a doc and then switch to another doc and the previous job is still running, it could replace it with previous doc properties
When a new document is loaded, I think this job should probably be cancelled, or at the very least the job should check that the input uri parameter matches the uri before replacing _documentProperties.value and _documentName.value
Note that DocumentPropertiesRetriever and its usage of PDFJsPropertiesToDocumentPropertyConverter are not suspending functions and they don't call cancellable functions (they seem to be pure Kotlin functions), so cancelling the job would only stop the job after it has finished converting when it enters withContext(Dispatchers.Main)
| 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); |
There was a problem hiding this comment.
I think this is from the page persistence branch, we should remove it
| mZoomRatio = 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)) | ||
| ); |
There was a problem hiding this comment.
Note that this pattern technically makes setting the zoom ratio asynchronous which changes the semantics. JS Bridge code should block JS exectuion when calling bridge methods, so offloading to Android main thread could result in later bridge getZoomRatio calls having a stale value
This is probably unlikely in practice depending on how far away subsequent getZoomRatio calls are from setZoomRatio calls. But I think we can just remove this runOnUiThread call and rely on zoomRatio being volatile
List of changes
Move UI operations to the main thread.
Make shared mutable state thread-safe.
Move file saving off UI thread.
Remove Loader. Document properties are loaded using ViewModel.
Remove
onSaveInstanceState. Persistent states are managed with ViewModel.Simplify Menu handling.
Display page number with Snackbar instead of Toast.
Remove Hungarian notation.