[TIMOB-25786] Android: WebView eval JS timeout error
GitHub Issue | n/a |
---|---|
Type | Bug |
Priority | None |
Status | Closed |
Resolution | Invalid |
Resolution Date | 2018-04-17T21:32:36.000+0000 |
Affected Version/s | Release 7.0.2 |
Fix Version/s | n/a |
Components | Android |
Labels | n/a |
Reporter | Victor Vazquez Montero |
Assignee | Joshua Quick |
Created | 2018-02-16T18:31:29.000+0000 |
Updated | 2018-04-17T21:32:36.000+0000 |
Description
Issue
When setting run-on-main-thread to true webview.evalJS errors with the following message:[WARN] TiWebViewBinding: (main) [4577,18530] Timeout waiting to evaluate JS
When setting run-on-main-thread to false webview.evalJS calls are executed as expected
This is important since the customer has to have run-on-main-thread in order to use Hyperloop
Steps to replicate
1. download project: [^evalJs-bug-demo.zip] 2. Run project to Android simulator and view console logAttachments
File | Date | Size |
---|---|---|
evalJs-bug-demo.zip | 2018-02-16T18:30:41.000+0000 | 723090 |
Hey [~amukherjee] can we have someone check out this ticket.
[~vvazquezmontero], Okay, I'll ask one of our Android devs to look into this.
This happens because we prevent the thread being blocked for more than 3500ms to prevent an ANR pop-up. https://github.com/appcelerator/titanium_mobile/blob/master/android/modules/ui/src/java/ti/modules/titanium/ui/widget/webview/TiWebViewBinding.java#L164 We should probably use a callback here instead
I think it's a deadlock due to the code being run on the main thread and not having special logic for handling it inline. I've never looked at this code before but it appears to make a special JS environment on the web view where it sets up a polling mechanism to grab code snippets off a queue from Java-land, then wraps the code in some wrapping JS code to eval it and set the result back on the java proxy. I imagine we'd have to retool this quite a bit to get it to work in a "main thread" way. As Gary suggested, we actually ran into this on Windows where the API is async and requires a second callback argument. I think it'd be a much simpler implementation to move this way on Android as well since they already have an async api to evaluate Javascript without all this polling script injection stuff: https://developer.android.com/reference/android/webkit/WebView.html#evaluateJavascript(java.lang.String,%20android.webkit.ValueCallback
Posted a PR with a possible async variant of the API that matches Windows. This would involve the developers/users passing a second argument to evalJS that was an async function that received the result, i.e.:
This PR is meant to get some feedback from Android devs, but may offer an alternative when using main thread. My attempt to spin off a thread to get this to work in a synchronous fashion with run-on-main-thread was unsuccessful.
[~vvazquezmontero], For now, you can work-around this issue by moving the
evalJS()
calls out of the custom "App.DOMContentLoaded" event and to theWebView
"load" event handler like this...I've isolated the issue. This is not a thread deadlock issue. What's happening is that the
WebView.evalJS()
function is getting called before the web page has finished loading. We inject aPOLLING_SCRIPT
to the page *+after+* it has finished loading viaTiWebViewClient.onPageFinished()
here... https://github.com/appcelerator/titanium_mobile/blob/master/android/modules/ui/src/java/ti/modules/titanium/ui/widget/webview/TiWebViewClient.java#L45 The polling script's job is to fetch a JavaScript snippet that has been added to the stack by theTiWebViewBinding.getJSValue()
method. This is the method that gets called byWebView.evalJS()
and blocks/waits for the polling script to fetch the snippet, execute it, and return a result. But since the page hasn't finished loading yet and this method blocks the main UI thread, the polling script never gets injected and the snippet in the stack is ignored. https://github.com/appcelerator/titanium_mobile/blob/master/android/modules/ui/src/java/ti/modules/titanium/ui/widget/webview/TiWebViewBinding.java#L151 Note that this is why my work-around from my previous post works. It's doing anevalJS()
after the page is loaded and after the polling script has been injected. Now that we know what's going on (and knowing is half the battle), we can look into fixing it. ;)Hey [~jquick] thanks for the update!!
[~vvazquezmontero], I'm going to write this up as a separate ticket and flag it as an enhancement. This isn't technically a bug because there are cases where doing a blocking
evalJS()
is impossible to execute, such as with [TIMOB-12095] where it's callingevalJS()
before the WebView is being displayed on-screen (I'm sure this would be impossible on iOS as well). So, the enhancements needed are: * Document thatevalJS()
should only be called after the page has been loaded. * ModifyfireEvent()
handling in HTML to be fired after the page has been loaded, not while loading. (This solves the customer case and makes it more convenient to use.) I'll write up the ticket later today.I'd like to offer one more work-around. On Android (and I mean only Android), you can async call a JavaScript function in your HTML by doing the following.
Note the "javascript:" URL scheme used above. This is how a native Android developer would do it on Android. Just remember that the above won't work on iOS. This is also something Titanium does not officially document, but it is a feature that Google officially documents for Android here... https://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object,%20java.lang.String) I'm bringing this up because
evalJS()
may still timeout when using a very large HTML page containing a lot of JavaScript on a horribly slow device. Even after you wait for the "load" event to be received. For a blockingevalJS()
function call, there is really not much more we can do. As in, there is no way to "fix" it. An async call is the only solution in such a case.I've written this up as an enhancement request here: [TIMOB-25859]
[~vvazquezmontero], Hold on. There's a much simpler solution. We already support async communications with the WebView on all platforms *+today+*. The HTML's JavaScript has access to APIs
Ti.App.addEventListener()
andTi.App.fireEvent()
. Titanium developers can also make up their own unique names for these events as well. So, instead of doing blockingevalJS()
calls, it would be far far better to do an event-driven approach between the WebView's JavaScript and Titanium's JavaScript. For example, if you want the Titanium side to invoke a JavaScript function on the HTML side, then you can do it like the below, which works on all platforms...And if you want to do an async
evalJS()
equivalent, that too can be done via events like the below. Again, the below works on all platforms.Now, the above solution will only work for HTML that's under your control versus some random webpage loaded from the Internet, but from a security standpoint that's probably for the best.