Handle Promise from Android WebView with evaluateJavascript - android

I'm working with the Android WebView and trying to handle the return of a JavaScript promise from the WebView on the Java side after calling it with evaluateJavascript.
document.java
Button buttonAnnotations = findViewById(R.id.buttonAnnotations);
buttonAnnotations.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
wv.evaluateJavascript("javascript:getAnnotations();", new ValueCallback<String>() {
#Override
public void onReceiveValue(String value) {
Toast.makeText(getApplicationContext(), value, Toast.LENGTH_SHORT).show();
}
});
}
});
index.html
async function getAnnotations() {
await pdfViewer.getAnnotations().then(result => {
return JSON.stringify(result ,null,2);
});
}
If I change the getAnnotations() function to not be async and return a string all works fine, so I'm trying to figure out how to handle this promise in the java code to get the result.
I've seen a few similar questions but none of the answers seemed to work in this case.

As I mentioned in the comment, you are returning a Promise from the async function getAnnotations (which cannot be used directly in onReceiveValue).
In order to "push" the result from getAnnotations back to Android, you have to use the JavascriptInterface:
In your Activity you can define:
#JavascriptInterface
public void onAnnotations(String result) {
Toast.makeText(WebViewActivity.this, result, Toast.LENGTH_LONG).show();
}
and register it with:
webView.addJavascriptInterface(this, "bridge");
In this case the method onAnnotations resists within the Activity which also contains the webView. Therefore I use "this" for the first Argument.
The "bridge" is the namespace, where you can find the function onAnnotations on the JavaScript side.
Now all you have to do, is to make the call from JavaScript to Android:
function getAnnotations() {
pdfViewer.getAnnotations().then(result => {
bridge.onAnnotations(JSON.stringify(result ,null,2));
});
}

Related

Android WebViewClient's ShouldInterceptRequest is never called in MAUI WebView

UPDATE: it's a confirmed bug. Please upvote it here because it doesn't really receive a lot of attention from MS.
I need to override the shouldInterceptRequest method of WebViewClient to load in-app HTML content following that guide.
Here's the repo with the reproducible code: GitHub. I took a sample code from MS Q&A as well:
// ...
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<Microsoft.Maui.Controls.WebView, ProblemHandler2>();
});
// ...
internal class ProblemHandler2 : WebViewHandler
{
protected override Android.Webkit.WebView CreatePlatformView()
{
var wv = new Android.Webkit.WebView(Android.App.Application.Context);
wv.SetWebViewClient(new CustomWebClient());
return wv;
}
}
In the repo, I included 2 custom handlers:
ProblemHandler2 is the exact snippet by the MSFT. I realized a problem: Setting MAUI WebView's Source property no longer navigates the real Android WebView:
WebViewHandler.Mapper.AppendToMapping("MyHandler", (handler, view) =>
{
#if ANDROID
var xWv = handler.PlatformView;
// For ProblemHandler2, this is needed to actually navigate:
xWv.LoadUrl("https://www.google.com/");
#endif
});
this.wv.Source = "https://www.google.com/";
ProblemHandler1 uses the default result and adds a custom handler. This fixes the navigation problem, but, both problem have the same issue:
ShouldInterceptRequest is never called. It is never called on anything even when I manually click a link to navigate. What am I missing? I am sure the CustomWebClient is correctly created and set.
I noticed none of the other callbacks works as well, for example:
public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
{
Debugger.Break();
Debug.WriteLine(url);
base.OnPageStarted(view, url, favicon);
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
Debugger.Break();
Debug.WriteLine(url);
base.OnPageFinished(view, url);
}
I also tried using WebViewHandler.Mapping but it also does not work:
WebViewHandler.Mapper.AppendToMapping("MyHandler", (handler, _) =>
{
#if ANDROID
handler.PlatformView.SetWebViewClient(new CustomWebClient());
#endif
});
I could be wrong but, I think this might have to do with your overridden version of the CreatePlatform method,
Can you try what the default WebViewHandler is doing:
protected override WebView CreatePlatformView()
{
var platformView = new MauiWebView(this, Context!)
{
LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
};
platformView.Settings.JavaScriptEnabled = true;
platformView.Settings.DomStorageEnabled = true;
platformView.Settings.SetSupportMultipleWindows(true);
return platformView;
}
Check this URL for the default handlers CreatePlatform setup :
https://github.com/dotnet/maui/blob/c6250a20d73e1992b4a02e6f3c26a1e6cbcbe988/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs
Also don't use Application Context in Handlers, Handlers have their own Context property you can use.
Yes, it is the case as you said.
And I have created a new issue for this problem, you can follow it up here: https://github.com/dotnet/maui/issues/11004.
Thanks for your support and feedback for maui.
Best Regards.

Issue with WebView.EvaluateJavaScript in Android Xamarin

I am using the following code for injecting Java Script in to my Android Web view
WebView
webView = FindViewById<WebView> (Resource.Id.learningWebView);
if (null != webView) {
webView.Settings.JavaScriptEnabled = true;
webView.Settings.SetSupportZoom (true);
webView.SetWebViewClient (new CustomWebViewClient ());
}
WebView Client implementation
public class CustomWebViewClient : WebViewClient
{
public override bool ShouldOverrideUrlLoading (WebView view, string url)
{
view.LoadUrl (url);
return true;
}
public override void OnPageStarted (WebView view, string url, Android.Graphics.Bitmap favicon)
{
}
public override void OnPageFinished (WebView view, string url)
{
base.OnPageFinished (view, url);
HideLearningDivs (view);
}
void HideLearningDivs (WebView view)
{
try {
view.EvaluateJavascript ("document.getElementById(\"suiteBar\").parentNode.style.display='none'", new JavaScriptResult ());
} catch (Exception ex) {
Console.WriteLine (ex.Message);
}
}
IValueCallback Implementation
public class JavaScriptResult : IValueCallback
{
public IntPtr Handle {
get;
set;
}
public void Dispose ()
{
}
public void OnReceiveValue (Java.Lang.Object result)
{
}
}
But during the time of executing the application I am getting the following error.
java.lang.NoSuchMethodError: no method with name='evaluateJavascript' signature='(Ljava/lang/String;Landroid/webkit/ValueCallback;)V' in class Landroid/webkit/WebView;
Can anyone please help me to find what is wrong with my implementation.
I will link to where I found the answer below, but basically you need to do a check for Android KitKat (4.4), since that function was not introduced until then. If the device is running lower than 4.4, then you may need to do something different to get the value back if you actually need to do something with it. Such as using a Hybrid WebView of some kind (check out Xamarin Forms Labs version of it perhaps) and/or using the AddJavaScriptInterface()
Here is the code:
if(Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat) {
webView.EvaluateJavascript("javascript:GoBack();", null);
} else {
webView.LoadUrl("javascript:GoBack();");
}
https://forums.xamarin.com/discussion/24894/webview-evaluatejavascript-issues
*Edit: Since writing this, I found an excellent post Adam Pedley (who I apparently have been linking to a lot lately) which covers doing this for Xamarin Forms but also mentions a change in Android 4.2 to the JS engine. Running the JavaScript might work the first time but it also sets the document object to the script result, so you may need to assign the result of document.getElementById() to a variable in order to work around this: var x = document.getElementById().
JavaScriptResult class must be inherited from Java.Lang.Object, like this:
public class JavaScriptResult : Java.Lang.Object, IValueCallback
{
public void OnReceiveValue(Object value)
{
// ...
}
}

Login function with ASync networking request?

I'm using 'Retrofit' for making asynchronous network requests, how might i right a function for handling logins? For instance i've currently attempted:
public UserAuthResponse Login(String username, String password) {
try {
Callback<UserAuthResponse> getAuthCallback = new Callback<UserAuthResponse>() {
#Override
public void failure(RetrofitError arg0) {
if (arg0 != null) {
if (arg0.getMessage() != null
&& arg0.getMessage().length() > 0) {
Log.e("KFF-Retrofit", arg0.getMessage());
}
}
}
#Override
public void success(UserAuthResponse listItem,
retrofit.client.Response arg1) {
Log.e("dg", listItem.getUser().getFirstname());
}
};
service.authUser(username, MD5(password), getAuthCallback);
return response;
} catch (RetrofitError e) {
e.printStackTrace();
return null;
}
}
But this is flawed: there is no way of returning the 'UserAuthResponse' from the function? How can i pass back the result?
It seems like i need a synchronous call to the web service but then i'm hit with a 'NetworkOnMainThreadException'
What is the best practice for things like this? Sorry about the poor explanation, struggling to form the right words.
Well the things is that when you're using the Callback as your means of getting the results from Retrofit you automatically giving away the possibility of having the response returned inline. There's a few ways this can be solved. I suppose it's up to you to choose which one fits best with your design.
You could decide to not use the Callback approach and use the inline result from Retrofit but then you'd need to handle the scheduling yourself otherwise you'll hit the Exception of NetworkOnMainThreadException like you mentioned.
You could also pass in a listener to your login method. This listener could then be called by the result Callback. This could be useful if you're trying to hide Retrofit behind some sort of service layer and expose a simple login interface.
interface OnLoginListener {
onLoginSuccessful(UserAuthResponse response);
onLoginFailed(Throwable t);
}
public void Login(String username, String password, final OnLoginListener listener) {
Callback<UserAuthResponse> getAuthCallback = new Callback<UserAuthResponse>() {
#Override
public void failure(RetrofitError e) {
// You can handle Retrofit exception or simply pass them down to the listener as is
listener.onLoginFailed(e);
}
#Override
public void success(UserAuthResponse listItem,
retrofit.client.Response arg1) {
// handle successful case here and pass down the data to the listener
listener.onLoginSucessful(listItem);
}
};
service.authUser(username, MD5(password), getAuthCallback);
}
use this line i Manifest
<uses-permission android:name="android.permission.INTERNET"/>
or use this before network operation (not suggestible)
if (Build.VERSION.SDK_INT>= 10) {
ThreadPolicy tp = ThreadPolicy.LAX;
StrictMode.setThreadPolicy(tp);
}

Sometimes throws Uncaught Error: Error calling method on NPObject on Android

I am having problems with the Webview in Android and it's JavascriptInterfaces.
I am passing a string to the JavascriptInterface. When debugging it, I receive the correct string within my Android application. The problem: Sometimes I get an Uncaught Error: Error calling method on NPObject.
Does anybody know why?
The Interface in Java:
public class JSInterfaceGame extends JSInterface {
#JavascriptInterface
public void setShareText(String share){
shareText = share;
if(mJSInterfaceListener != null)
mJSInterfaceListener.onParametersChanged(SHARE_TEXT);
}
The initialization in the onCreateView-Method within the Fragment:
online = (WebView) rootView.findViewById(R.id.online);
online.setWebViewClient(new WISWebviewClient() {
#Override
public void onStatusChanged(final WebView view, int progress, long duration) {
//unrelated
}
});
WebSettings ws = online.getSettings();
ws.setJavaScriptEnabled(true);
ws.setUserAgentString(USER_AGENT);
ws.setCacheMode(WebSettings.LOAD_DEFAULT);
ws.setRenderPriority(WebSettings.RenderPriority.HIGH);
SharedPreferences settings = getActivity().getSharedPreferences(GameActivity.PREFERENCES, Context.MODE_PRIVATE);
mJSInterface = new JSInterfaceGame();
mJSInterface.setJSInterfaceListener(this); // Defined elsewhere in this class.
mJSInterface.setPlayerName(settings.getString(GameActivity.PREFS_PlAYERNAME, null));
online.addJavascriptInterface(mJSInterface, "JSInterface");
online.loadUrl("http://myurl.something");
Call in Javascript:
function makeShareText() {
var text = "Some text";
console.log(typeof text); // Always a string.
JSInterface.setShareText(text);
}
It happens when you try, using method called from javascript interface, to interact with UI. To solved it in this way:
class mJSInterface()
{
public void myFunction()
{
runOnUiThread(new Runnable() {
public void run() {
//Code that interact with UI
}
});
}
}
To highlight the comment from #Leog
The same error occurs if you call the native javascript function with wrong parameters
This was the source of my error
Another reason can be a RuntimeException on a WebViewCoreThread. Any exception occurred after receiving #JavascriptInterface call will be logged as NPObject error if still running on a WebView thread. Overall insufficient trace message with little clue about the problem.
Correct your issue with handling javascript interface call on a suitable thread.
Example A. (NPObject error):
#JavascriptInterface
public void jsCall() {
Log.v(TAG, "Prepared NullPointerException on "+Thread.currentThread());
String s = null;
s.length(); // This will cause NPObject error
}
Example B. (NullPointerException):
#JavascriptInterface
public void jsCall() {
new Thread(new Runnable() {
#Override
public void run() {
Log.v(TAG, "Prepared NullPointerException on " + Thread.currentThread());
String s = null;
s.length(); // This will throw NullPointerException
}
}).start();
}
Take this as an addition to #Nico.S's answer.
Operate iframe in Android 4.4 WebView may cause a similar exception(Uncaught ReferenceError: NPObject deleted), finally I find out the solution:
#Override
public void onPageFinished(final WebView view, String finishUrl) {
super.onPageFinished(view, finishUrl);
// android 4.4 may lost value of 'Android' when operating iframe
view.addJavascriptInterface(Activity.this, "Android");
}

Android addJavascriptInterace, calling native function gives me a typeError

Ok guys. What I'm trying to do: I've got some backend code written in javascript (the project was originally supposed to be a web-app) and I've added a javascript interface to my android web view, and when I press a button in my ui, the javascript code does some stuff, then calls a function in my javascript interface when it is done... The code from my javascript interface:
public void onReady() {
backend.loadUrl("javascript:dokus.backend.onLoginStatusChange(Bridge.onLoginStatusChange);");
backend.loadUrl("javascript:dokus.backend.onProjectsChange(Bridge.onProjectsChange);");
backend.loadUrl("javascript:dokus.backend.onLatestEntriesChange(Bridge.onLatestEntriesChange);");
}
public void onLoginStatusChange (boolean loggedIn, String error) {
Toast.makeText(mainActivity, "LoginStatusChange", Toast.LENGTH_SHORT).show();
}
...
I've already verified that the onReady function is called, so that's not the problem.
The javascript function which calls onLoginStatusChange is this following:
function _login_callback(loggedIn, msg) {
if (loggedIn) {
_get_projects();
_get_latest_entries();
}
console.log(_login_status_changed_callback);
if (_login_status_changed_callback !== undefined)
_login_status_changed_callback (loggedIn, msg);
}
The error i get is this: "Line: 180, type error". That's all i get from my WebChromeView function, that prints everything from console.log() to a toast message.
the console.log(_login_status_changed_callback) prints out the following:
function onLoginStatusChange () {
[native code]
}
I've verified that the correct arguments are called to the function (a bool and a string), so that shouldn't be the error either... I'm at a loss, what do i do?
setup code for the web view:
private void setupBackend () {
final DokusActivity that = this;
backend = new WebView(this);
BackendBridge.getInstance().initInstance(this, backend);
backend.getSettings().setJavaScriptEnabled(true);
backend.getSettings().setDomStorageEnabled(true);
backend.addJavascriptInterface(BackendBridge.getInstance(), "Bridge");
backend.setWebViewClient(new WebViewClient() {
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
Toast.makeText(that, "Oh no! " + description, Toast.LENGTH_SHORT).show();
}
});
backend.setWebChromeClient(new WebChromeClient() {
public boolean onConsoleMessage (ConsoleMessage consoleMessage) {
Toast.makeText(that, "Oh no! " + consoleMessage.lineNumber() + ": " + consoleMessage.message(), Toast.LENGTH_SHORT).show();
return false;
}
});
backend.loadUrl("file:///android_asset/index.html");
}
Apparently, the webview has issues when you try to pass native functions as callbacks to other methods. What I did was create a new .js file which works as callbacks, all the js callbacks do, is calling the native method.

Categories

Resources