-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Introduce blob handling for Android & iOS #8202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
riderx
wants to merge
22
commits into
ionic-team:main
Choose a base branch
from
Cap-go:blob-handling-update
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
76500c3
Improved solution for iOS 14.5 >=
gwdp a2b9de2
Improvements to file writting (avoid collision and empty file name (m…
gwdp a2de621
Android code working with encoding and mime type issues (also no webh…
gwdp f353ddf
More changes relative to android download, implement activity with fi…
gwdp 79cd81e
Working thread operation with content type resolution from XHR
gwdp 1f01c45
Intercept known chrome mimetype blobs and spawn same download process
gwdp b2ef3b5
Fix mime type issue and copy to capacitor
gwdp e71a8d2
Tweaks on android + add notification to application (yet, needs plugi…
gwdp 93efbff
Improve handler to also download common mime type blobs
gwdp b9ffb6d
Add folder picker for iOS
gwdp e6afdc1
Fix Swiftlint warnings
gwdp efe51b6
Fix Java ESlint
gwdp a6ba8b9
Fix compiler errors after fixes on swift lint
gwdp c4e53ad
Fix compile errors on Xcode 12.4 and bellow
gwdp 7020e28
Merge branch 'main' into blob-handling-update
riderx 6c04ea9
Merge branch 'main' into blob-handling-update
riderx 74f9323
Merge branch 'main' into blob-handling-update
riderx 55443c4
fix: stop overriding Bridge service worker handling in download proxy
TorichanCapgo f44fcd5
fix: address remaining blob-download review comments
TorichanCapgo 13bc703
fix: post download status as Int raw values for ObjC consumers
TorichanCapgo 2759939
fix: address remaining blob-download review comments
TorichanCapgo 60a593d
fix: address remaining blob-download review comments
TorichanCapgo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
android/capacitor/src/main/java/com/getcapacitor/DownloadJSInterface.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| package com.getcapacitor; | ||
|
|
||
| import android.app.Activity; | ||
| import android.webkit.JavascriptInterface; | ||
| import androidx.activity.result.ActivityResultLauncher; | ||
| import androidx.annotation.Nullable; | ||
| import java.util.HashMap; | ||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Represents the bridge.webview exposed JS download interface + proxy interface injector. | ||
| * Every download request from webview will have their URLs + mime, content-disposition | ||
| * analyzed in order to determine if we do have a injector that supports it and return | ||
| * to the proxy in order to have that code executed exclusively for that request. | ||
| */ | ||
| public class DownloadJSInterface { | ||
|
|
||
| private final DownloadJSOperationController operationsController; | ||
| private final ActivityResultLauncher<DownloadJSOperationController.Input> launcher; | ||
| private final HashMap<String, DownloadJSOperationController.Input> pendingInputs; | ||
| private final Bridge bridge; | ||
|
|
||
| public DownloadJSInterface(Bridge bridge) { | ||
| this.operationsController = new DownloadJSOperationController(bridge.getActivity()); | ||
| this.pendingInputs = new HashMap<>(); | ||
| this.bridge = bridge; | ||
| this.launcher = | ||
| bridge | ||
| .getActivity() | ||
| .registerForActivityResult( | ||
| this.operationsController, | ||
| result -> Logger.debug("DownloadJSActivity result", String.valueOf(result)) | ||
| ); | ||
| } | ||
|
|
||
| /* JavascriptInterface imp. */ | ||
| @JavascriptInterface | ||
| public void receiveContentTypeFromJavascript(String contentType, String operationID) { | ||
| this.transitionPendingInputOperation(operationID, contentType, false); | ||
| } | ||
|
|
||
| @JavascriptInterface | ||
| public void receiveStreamChunkFromJavascript(String chunk, String operationID) { | ||
| this.transitionPendingInputOperation(operationID, null, false); | ||
| this.operationsController.appendToOperation(operationID, chunk); | ||
| } | ||
|
|
||
| @JavascriptInterface | ||
| public void receiveStreamErrorFromJavascript(String error, String operationID) { | ||
| // Drop pending input without launching the file picker or emitting STARTED. | ||
| this.pendingInputs.remove(operationID); | ||
| this.operationsController.failOperation(operationID); | ||
| this.bridge.getApp().fireDownloadUpdate(operationID, App.DownloadStatus.FAILED, error); | ||
| } | ||
|
|
||
| @JavascriptInterface | ||
| public void receiveStreamCompletionFromJavascript(String operationID) { | ||
| if (!this.operationsController.completeOperation(operationID)) return; | ||
| this.bridge.getApp().fireDownloadUpdate(operationID, App.DownloadStatus.COMPLETED, null); | ||
| } | ||
|
|
||
| /* Proxy injector | ||
| * This code analyze incoming download requests and return appropriated JS injectors. | ||
| * Injectors, handle the download request at the browser context and call the JSInterface | ||
| * with chunks of data to be written on the disk. This technic is specially useful for | ||
| * blobs and webworker initiated downloads. | ||
| */ | ||
| public String getJavascriptBridgeForURL(String fileURL, String contentDisposition, String mimeType) { | ||
| if (fileURL.startsWith("http://") || fileURL.startsWith("https://") || fileURL.startsWith("blob:")) { | ||
| String operationID = UUID.randomUUID().toString(); | ||
| DownloadJSOperationController.Input input = new DownloadJSOperationController.Input( | ||
| operationID, | ||
| fileURL, | ||
| mimeType, | ||
| contentDisposition | ||
| ); | ||
| this.pendingInputs.put(operationID, input); | ||
| return this.getJavascriptInterfaceBridgeForReadyAvailableData(fileURL, mimeType, operationID); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /* Injectors */ | ||
| private String getJavascriptInterfaceBridgeForReadyAvailableData(String blobUrl, String mimeType, String operationID) { | ||
| String escapedUrl = blobUrl.replace("\\", "\\\\").replace("'", "\\'"); | ||
| String acceptHeader = | ||
| (mimeType != null && mimeType.length() > 0) ? "xhr.setRequestHeader('Accept','" + mimeType.replace("'", "") + "');" : ""; | ||
| return ( | ||
| "javascript: " + | ||
| "function parseFile(file, chunkReadCallback, errorCallback, successCallback) {\n" + | ||
| " let fileSize = file.size;" + | ||
| " let chunkSize = 64 * 1024;" + | ||
| " let offset = 0;" + | ||
| " let readBlock = null;" + | ||
| " let onLoadHandler = function(evt) {" + | ||
| " if (evt.target.error == null) {" + | ||
| " var buf = evt.target.result;" + | ||
| " var bytes = new Uint8Array(buf);" + | ||
| " offset += bytes.length;" + | ||
| " var binary = '';" + | ||
| " for (var i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); }" + | ||
| " chunkReadCallback(binary);" + | ||
| " } else {" + | ||
| " errorCallback(evt.target.error);" + | ||
| " return;" + | ||
| " }" + | ||
| " if (offset >= fileSize) {" + | ||
| " if (successCallback) successCallback();" + | ||
| " return;" + | ||
| " }" + | ||
| " readBlock(offset, chunkSize, file);" + | ||
| " };" + | ||
| " readBlock = function(_offset, length, _file) {" + | ||
| " var r = new FileReader();" + | ||
| " var blob = _file.slice(_offset, length + _offset);" + | ||
| " r.onload = onLoadHandler;" + | ||
| " r.readAsArrayBuffer(blob);" + | ||
| " };" + | ||
|
Comment on lines
+113
to
+118
|
||
| " readBlock(offset, chunkSize, file);" + | ||
| "};\n" + | ||
| "(() => { let xhr = new XMLHttpRequest();" + | ||
| "xhr.open('GET', '" + | ||
| escapedUrl + | ||
| "', true);" + | ||
| acceptHeader + | ||
| "xhr.responseType = 'blob';" + | ||
| "xhr.onerror = function(e) {" + | ||
| " var msg = (e && e.type) ? e.type : 'network error';" + | ||
| " console.error('[Capacitor XHR] - error:', msg);" + | ||
| " CapacitorDownloadInterface.receiveStreamErrorFromJavascript(msg, '" + | ||
| operationID + | ||
| "');" + | ||
| "};" + | ||
| "xhr.onload = function(e) {" + | ||
| " if (this.status == 200) {" + | ||
| " let contentType = this.getResponseHeader('content-type');" + | ||
| " if (contentType) { CapacitorDownloadInterface.receiveContentTypeFromJavascript(contentType, '" + | ||
| operationID + | ||
| "'); }" + | ||
| " var blob = this.response;" + | ||
| " parseFile(blob, " + | ||
| " function(chunk) { CapacitorDownloadInterface.receiveStreamChunkFromJavascript(chunk, '" + | ||
| operationID + | ||
| "'); }," + | ||
| " function(err) { console.error('[Capacitor XHR] - error:', err); CapacitorDownloadInterface.receiveStreamErrorFromJavascript(err && err.message ? err.message : 'Unknown error', '" + | ||
| operationID + | ||
| "'); }, " + | ||
| " function() { console.log('[Capacitor XHR] - Drained!'); CapacitorDownloadInterface.receiveStreamCompletionFromJavascript('" + | ||
| operationID + | ||
| "'); } " + | ||
| " );" + | ||
| " } else {" + | ||
| " var msg = 'HTTP ' + this.status;" + | ||
| " console.error('[Capacitor XHR] - error:', this.status, (e ? e.loaded : this.responseText));" + | ||
| " CapacitorDownloadInterface.receiveStreamErrorFromJavascript(msg, '" + | ||
| operationID + | ||
| "');" + | ||
| " }" + | ||
| "};" + | ||
| "xhr.send();})()" | ||
| ); | ||
| } | ||
|
|
||
| /* Helpers */ | ||
| private void transitionPendingInputOperation(String operationID, @Nullable String optionalContentType, boolean doNotStart) { | ||
| DownloadJSOperationController.Input input = this.pendingInputs.get(operationID); | ||
| if (input == null) return; | ||
| if (optionalContentType != null) { | ||
| Logger.debug("Received content type", optionalContentType); | ||
| input.optionalMimeType = optionalContentType; | ||
| } | ||
| this.pendingInputs.remove(operationID); | ||
| Activity activity = bridge.getActivity(); | ||
| if (activity == null) return; | ||
| activity.runOnUiThread( | ||
| () -> { | ||
| if (!doNotStart) { | ||
| launcher.launch(input); | ||
| bridge.getApp().fireDownloadUpdate(operationID, App.DownloadStatus.STARTED, null); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
receiveStreamErrorFromJavascriptcallstransitionPendingInputOperation(..., doNotStart=true), which removes the pending input but still fires a STARTED update and (because the operation was never launched)failOperationwill typically return false, preventing a FAILED notification. The error path should either start the operation before failing, or avoid firing STARTED whendoNotStartis true and ensure FAILED is still emitted/cleaned up.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed: errors no longer launch the picker or emit STARTED.
receiveStreamErrorFromJavascriptdrops the pending input and fires FAILED.