WebView Javascript

u137a

Member
Licensed User
Longtime User
The WebView has Javascript enabled by default.

Is there a way to exchange messages between Javascript and B4A? This would be useful for HTML forms and advanced user interfaces, especially since phones are now getting HTML5

Cheers.
 

warwound

Expert
Licensed User
Longtime User
Hi Erel.

I'm struggling to find a solution to this problem.

I see that the native WebView object has an addJavascriptInterface() method and this is exactly what is needed.

I see a few blogs and tutorials that give examples of using the addJavascriptInterface() method but nothing basic enough for me to follow.
(My knowledge of Java is very limited!)

How easy would it be for a newbie like me to customise the existing B4A WebView adding extra javascript methods?

Thanks.

Martin.

[edit]I tried to post links to the official documentation for the addJavascriptInterface() method but cannot as i have posted less than 5 times on the forum. Google is your friend![/edit]
 

warwound

Expert
Licensed User
Longtime User
OK.

I have paid for the full version so will look at using the Reflector library.

Thanks.

Martin.
 

agraham

Expert
Licensed User
Longtime User
As this looked technically interesting I've had a quick look at this and I am afraid there might be multiple issues to solve if you are intending to call Basic4android methods from Javascript code (about which I know absolutely nothing! I usually avoid Webby things like the plague).

Firstly a couple of general very important Android issues I found on the web.
Remember that when methods of the passed java object is called in javascript context, it's not gonna be invoked in the Main(UI) thread, and if you need to get it called in the UI thread you will need to use a Handler and post a new runnable to run your code.
...
another important thing to remember is the fact that you cannot pass or return any complex object to or from javascript context, that is all you are allowed to use are primitive and String data types.
So your Basic4android Subs can't touch the UI, maybe a big deal, and can only accept primitives, probably not such a big deal.

Secondly addJavascriptInterface() needs an object instance whose methods it will call. Basic4android methods belong to either an activity, code or module Class, With the Reflection library you can pick up an instance of any of these using Reflector.GetMostCurrent so that instance might be able to be passed. Compiled Basic4android method names are different to the source code names but this should be no problem as they are predictable and fortunately Basic4android methods are declared Public so they should be visible externally. A consideration that might stop this working is they are also declared Static and while in Java static methods can be called on a class instance (the B4A architecture relies on this) I haven't been able to establish whether they can be called from Javascript. As I expect that reflection on the provided instance is used to locate methods to invoke them they should be visible to and callable from Javascript unless it does an explicit check for non-static methods.

However even if it doesn't work out of the tin it should be possible with a Basic4android library to provide an interface to addJavascriptInterface(). If you write one of those Webby program things that you think ought to work but doesn't then post it here and I'll have a play. For your attempt note that Basic4android method names are all lower case and comprise an underscore followed by the Sub name. So "Sub Fred" is compiled as "_fred".
 
Last edited:

bluejay

Active Member
Licensed User
Longtime User
Hi agraham,

The original request was how to pass "Messages" between Javascript and B4A, the UI on the HTML side should be managed by the Javascript including in response to messages.

The free Google Scripting Layer For Android (SL4A) supports messaging between Javascript and a dozen scripting languages which implies the functionality is there on the Javascript side and we just need to connect the B4A side ie. to be able to GET and POST messages from a common message queue and to generate an event when a message arrives.

The examples in SL4A would be a good place to start if you are looking for Webby program things to test with.

Cheers
 

agraham

Expert
Licensed User
Longtime User
The purpose of my post was to comment on the use of addJavascriptInterface() by means of the Reflection library alluded to by above by warwound and Erel.

(SL4A) supports messaging between Javascript and a dozen scripting languages
I don't see that unless I have missed something. SL4A appears to be a mechanism for exposing the Android API to interpreters that normally would not be able to access it and also providing a script editor for on device development of scripts for those interpreters.
 

bluejay

Active Member
Licensed User
Longtime User
This is from the SL4A WiKi.

"If you decide to launch your WebView from an interpreter, such as Python, you can communicate back and forth between the WebView and your Python script via events. Events posted in either the Python script or WebView's JavaScript can be received by the other side. "

Full text and examples at this link.

UsingWebView - android-scripting - A guide to HTML scripts and using WebView APIs. - Scripting Layer for Android brings scripting languages to Android. - Google Project Hosting

Do you think it is possible to replicate this Python behavior in B4A with just Reflection or is a library required?
 

agraham

Expert
Licensed User
Longtime User
I don't think that is any old WebView. I think it is specially wrapped full screen WebView, like a dialog, that you can launch from the interpreter. That WebView is displayed and managed by the SL4A process which is a separate process to the interpreter process.

It is not possible to access the SL4A process using reflection. An interpreter is not a normal application. It is SL4A that starts the interpreter, in response to a user selection, whose existence it knows about from manifest entries and broadcast intents so the interpreter is not an application as Basic4andoid know it but is more like a content provider with SL4A doing the UI functions. A Basic4android library alone cannot provide the required functionality.

There are further things complexities involved in providing an interpreter that SL4A can invoke but they are far too numerous to describe here, see "The way of Zen" in the InterpreterDeveloperGuide for a flavour. I could go on more but it is a non-starter as far as Basic4android is concerned.
 

warwound

Expert
Licensed User
Longtime User
However even if it doesn't work out of the tin it should be possible with a Basic4android library to provide an interface to addJavascriptInterface(). If you write one of those Webby program things that you think ought to work but doesn't then post it here and I'll have a play. For your attempt note that Basic4android method names are all lower case and comprise an underscore followed by the Sub name. So "Sub Fred" is compiled as "_fred".

Hi agraham.

I've tried a simple test to call the WebView addJavascriptInterface method but always get an error:

B4X:
java.lang.NoSuchMethodException: addJavascriptInterface

Here's my activity:

B4X:
'Activity module
Sub Process_Globals
   'These global variables will be declared once when the application starts.
   'These variables can be accessed from all modules.

End Sub

Sub Globals
   'These global variables will be redeclared each time the activity is created.
   'These variables can only be accessed from this module.
   Dim myWebView As WebView
End Sub

Sub Activity_Create(FirstTime As Boolean)

End Sub

Sub Activity_Resume
   Activity.LoadLayout("layoutMain")
   
   myWebView.Width=100%x
   myWebView.Height=100%y
   myWebView.LoadUrl("file:///android_asset/my_web_page.htm")
   
End Sub

Sub Activity_Pause (UserClosed As Boolean)

End Sub

Sub myWebView_PageFinished (Url As String)
   Dim Obj1 As Reflector
   Obj1.Target=myWebView
   '   all attempts to run the method produce an error:
   '   java.lang.NoSuchMethodException: addJavascriptInterface
   '   Obj1.RunMethod("addJavascriptInterface")
   Obj1.RunMethod2("addJavascriptInterface", "myNewJSMethod", "java.lang.String")
End Sub

And i have a code module where i intended to define the various new javascript methods:

B4X:
'Code module
'Subs in this code module will be accessible from all modules.
Sub Process_Globals
   'These global variables will be declared once when the application starts.
   'These variables can be accessed from all modules.

End Sub

Sub myNewJSMethod(myString As String)
   '   this will be the new javascript method
   Log("This is the new javascript method executed")
   Log(myString)
End Sub

You can see i tried the Reflector library's RunMethod and RunMethod2 methods but always get the same error.

Can you advise?

Thanks a lot.

Martin.
 

Attachments

  • addJavascriptInterface.zip
    6.6 KB · Views: 472

agraham

Expert
Licensed User
Longtime User
I don't think you understand how addJavascriptInterface() works and how to use it. You are getting the error because you have got the method signature wrong and indeed there is no method with the signature you are giving it. This code runs without error but I don't know if it will let you invoke Basic4android Subs from within Javascript. "main" below is the name in lowercase of the module containing the methods you want to invoke. "B4A" below is the prefix name to use within Javascript for the methods you want to invoke.

B4X:
   Dim Obj1 As Reflector
   Dim Obj2 As Reflector
   
   Obj1.Target=WebV
   Obj2.Target = Obj2.GetMostCurrent("main")
   Msgbox(Obj1.ToString, "Target 1")
   Msgbox(Obj2.ToString, "Target 2")
   Obj1.RunMethod3("addJavascriptInterface", Obj2.Target, "java.lang.Object", "B4A", "java.lang.String")
 

warwound

Expert
Licensed User
Longtime User
Hi.

I've been trying to get this to work over the weekend but had little luck so far.
I abandoned the previously posted code as it just didn't seem to create the required Java class.
The browser never reported any new interface.

So i've tried to create the new javascript interface as a library.

Here's part of my activity code:

B4X:
Sub Activity_Resume
   Activity.LoadLayout("layoutMain")
   myWebView.Width=100%x
   myWebView.Height=100%y
   
   Dim Obj1 As Reflector
   Obj1.Target=myWebView
   Dim interface As JSInterface
   
   Obj1.RunMethod3("addJavascriptInterface", interface, "java.lang.Object", "myinterface", "java.lang.String")
   
   myWebView.LoadUrl("file:///android_asset/my_web_page.htm")
End Sub

And here's my library code:

B4X:
package martinpearman.co.uk.jsinterface;

import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;
import anywheresoftware.b4a.BA.Author;
import android.util.*;

@ShortName("JSInterface")
@Version(1)
@Author("Martin Pearman")

public class JSInterface {
   public JSInterface(){}
   public String newMethod(){
      Log.v("martin", "hello console");
      String str="It works!";
      return str;
   }
   public String ToString(){
      return "My debug message";
   }
   public String newMethod2(){
      return "new method2";
   }
}

And my web page:

B4X:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>JSMethods addJavascriptInterface test page</title>
<script type="text/javascript">
function init(){
   //   the page has loaded, lets try and call the new javascript method
   var html=[];
   if(document.myinterface){
      html.push('"document.myinterface" IS defined');
      html.push('Properties of document.myinterface are:');
      for(var property in document.myinterface){
         html.push(property);
      }
      html.push('End of document.myinterface properties');
   } else {
      html.push('"document.myinterface" is NOT defined');
   }
   if(window.myinterface){
      html.push('"window.myinterface" IS defined');
      html.push('Properties of window.myinterface are:');
      for(var property in window.myinterface){
         html.push(property);
      }
      html.push('End of window.myinterface properties');
   } else {
      html.push('"window.myinterface" is NOT defined');
   }
   document.getElementById('myDiv2').innerHTML=html.join('<br />');
   document.getElementById('myDiv').innerHTML+=' debug #2';
   try{
      document.getElementById('myDiv').innerHTML+=window.myinterface.newMethod2()+' debug';
   } catch(error){
      document.getElementById('myDiv').innerHTML=error;
   }
   document.getElementById('myDiv').innerHTML+=' debug #3';
}
</script>
</head>

<body>
<div id="myDiv">debug #1</div>
<a href="javascript:void(0)" onclick="init()">Click me</a>
<div id="myDiv2"></div>
</body>
</html>

Now the web page code reports that 'document.myinterface' is not defined BUT 'window.myinterface' IS defined so i think i'm heading in the right direction!
The web page also reports that although window.myinterface is defined, it has no properties or methods.

What exactly is the result of Dim interface As JSInterface?
Is it a class or an instance of the class?

The IDE shows auto-complete for the three methods of my class - newMethod(), ToString() and newMethod2(), but the web page reports these functions as not defined.

Should my library be restructured so that it returns an instance of the javascript interface class?
Pseudo code like this (doesn't work but my Java skills are no doubt too limited);

B4X:
public class JSInterface {
   public JSInterface(){}
   public newInterface getInterface(){
      return new newInterface();
   }
}

class newInterface{
   public String newMethod(){
      Log.v("martin", "hello console");
      String str="It works!";
      return str;
   }
   public String ToString(){
      return "My debug message";
   }
   public String newMethod2(){
      return "new method2";
   }
}

Then in my activity i'd try something like:

B4X:
Dim interface As JSInterface
   
   Obj1.RunMethod3("addJavascriptInterface", interface.getInterface, "java.lang.Object", "myinterface", "java.lang.String")

Thanks.

Martin.
 

Attachments

  • addJavascriptInterface.zip
    6.4 KB · Views: 400

agraham

Expert
Licensed User
Longtime User
From the web, in your case "FileUtil" is "myinterface".

Dim does provide an instance of your class. There is no need for anything more complicated.

Dump the "window." -- just reference FileUtil directly.

> Then I displayed all members of the window object and my "FileUtil" is
> missing.


It's not supposed to be there. You do not get an actual Javascript
object from addJavascriptInterface().

It is a bit more like you get a new Javascript keyword -- you can write
"FileUtil.read()", and the Javascript interpreter will interpret it, but
there is no FileUtil object.
If you end up being able to call methods from your library I can see no reason why you couldn't call Basic4android Subs using my original code fragment as long as you lowercase the names and prefix them with an underscore.
 
Last edited:

warwound

Expert
Licensed User
Longtime User
At last i got it working :sign0060:!

I created two classes in my library:

B4X:
package martinpearman.co.uk.jsinterface;

import android.webkit.WebView;
import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;
import anywheresoftware.b4a.BA.Author;

@ShortName("JSInterface")
@Version(1)
@Author("Martin Pearman")

public class JSInterface {
   static NewInterface thisInterface=new NewInterface();
   public void addInterface(WebView myWebView, String interfaceName){
      myWebView.addJavascriptInterface(thisInterface, interfaceName);
   }
}

and

B4X:
package martinpearman.co.uk.jsinterface;

final class NewInterface {
   public String newMethod(String myString) {
      myString=ReverseString.reverseIt(myString);
      return myString;
   }
}

class ReverseString {
   public static String reverseIt(String source) {
      int i, len = source.length();
      StringBuffer dest = new StringBuffer(len);

      for (i = (len - 1); i >= 0; i--)
         dest.append(source.charAt(i));
      return dest.toString();
   }
}

My activity resume code is:

B4X:
   Activity.LoadLayout("layoutMain")
   myWebView.Width=100%x
   myWebView.Height=100%y
   
   Dim interface As JSInterface
   interface.addInterface(myWebView, "myinterface")
   
   myWebView.LoadUrl("file:///android_asset/my_web_page.htm")

I pass the WebView object to the library where the javascript interface is added.

Unfortunately this means that the methods created by the new javascript interface are hardcoded into the library so it's not a library that will be of much use to others.

I tried a different technique.
I added a codemodule to my project and tried to pass it to the libary as the new javascript interface to add - the theory being that anyone could write a code module and pass it to the library creating their own new javascript methods without having to rebuild the library.
That didn't work.

I shall experiment more and if i devise a way for others to create a javascript interface in B4A (ie the interface is no longer hardcoded) then i'll update this thread.

Martin.
 

agraham

Expert
Licensed User
Longtime User
I added a codemodule to my project ... That didn't work.
I would have thought it should have as long as you got an instance from Reflector.GetGetMostCurrent and used the internal Basic4android method names. Maybe it's because the Basic4android methods are static. If you want to post a small app with a web page that calls into your library I would be happy play to see whether it is possible to call into a code module from Javascript.

There is a mechanism you can use to invoke arbitrary Basic4android methods from your library and so from Javascript. It is in the BAShared library BA object and the signature is
B4X:
public Object raiseEvent(Object sender, String event, Object[] params) {
The compiler will give you a BA instance if you want one by using the invisible (to the Basic4android programmer) BA parameter in a method call.

B4X:
private BA ba
public void Initialize(BA aba)
{
    ba = aba;
}

// can't pass Object arrays from Javascript
// Javascript might need String or primitive and not Object return types - I don't know.
// If it does add a cast to the expected type to the return from raiseEvent

public Object CallSub0(String sub)
{
    sub = sub.toLowerCase();
    return ba.raiseEvent(this, sub, new Object[0]);
}

public Object CallSub1(String sub, String param1)
{
    sub = sub.toLowerCase();
    return ba.raiseEvent(this, sub, new Object[] = { param1});
}

...
The sub name can be any of the Subs in module whose BA you have, that is the module that declared and initialised your library object. I think the code above is OK but I haven't actually run it.

Remember that the subs in your library don't run on the UI thread and so are asynchronous relative to your program main thread. It is possible to overcome that limitation if necessary.
 

warwound

Expert
Licensed User
Longtime User
I've attached a basic app that tries to use a code module as the new javascript interface - ModuleInterface.zip.

Look at the webpage after clicking the link on it and you'll see that moduleinterface has been created, but calling it's _newmethod() method returns nothing to the javascript from the newMethod() sub and that the log doesn't indicate that newMethod() sub has even been called.

Previously i'd been getting errors such as moduleinterface._newmethod() is undefined so the code may still be workable.

I did try adding the activity Main module as the javascript interface - adding it within the Main module itself.
I called newMethod2() and the app crashed - lots of output in the log window but a bit too obscure for me to debug lol!

So i'm now developing the working code - where the library contains the new class to be used as the javascript interface.
It's attached as B4AJSInterface.zip.

I'm thinking that the new (hardcoded) interface could function:

Send data from B4A to javascript - the interface would trigger a javascript event and the javascript would ask the interface for the data when the event fires.

Send data from javascript to B4A - the interface library instance could listen for an event such as:

B4X:
Sub myJSInterfaceInstance_messageReceived(messageName As String, message As String)
' code here
End Sub

So i can hopefully send a command (messageName) and a JSON string (the message) from the javascript to B4A where it can be processed.

I'm stuck here!

You said:

The compiler will give you a BA instance if you want one by using the invisible (to the Basic4android programmer) BA parameter in a method call.

In the example code you posted public void Initialize(BA aba), whereabouts is this Initialize method called from - that is how do i get a reference to the BA object?

FYI, values returned from the Java class to the javascript are (it seems) always javascript objects, that is:

B4X:
typeof valueReturnedFromJavaMethodToJS is always 'object'

If a value is known to be a number then parseInt() or parseFloat() will cast it to the desired type.

Values that are known to be strings can be cast to string:

B4X:
value = value + '';

Thanks a lot for any help you can give.

Martin.
 

Attachments

  • B4AJSInterface.zip
    10.6 KB · Views: 492
  • ModuleInterface.zip
    9.6 KB · Views: 388

agraham

Expert
Licensed User
Longtime User
Personally I would ignore the hardcoded interface, providing you can get the CallSub mechanism to work, as it is more flexible. There is no reason for it not to work as both necessary parts are in place. Your Javascript can call a library function so that function can call into B4A code. Several of my libraries use RaiseEvent to call back into Basic4Android so I know that works. Keeping all parameters and returns as strings keeps things simple.
Send data from B4A to javascript - the interface would trigger a javascript event and the javascript would ask the interface for the data when the event fires.
That's an interesting possibility. What is the mechanism for external code to trigger a Javascript event. I haven't seen any reference to that possibility while Googling for Javascrip information.

Send data from javascript to B4A - the interface library instance could listen for an event
This is exactly the same as the Javascript calling a CallSub method in the library that then calls the B4A Sub. The only difference is that the name of the called Sub is decided by the Javascript and not by the library code.

In the example code you posted public void Initialize(BA aba), whereabouts is this Initialize method called from .

Just like a normal B4A object
B4X:
Dim Whatever As JSInterface
Whatever.Initialize '  the compiler adds the BA instance parameter to the call
EDIT :- If you have an existing Initialise method because you need other initialisation just add the BA parameter to that.
 
Last edited:

agraham

Expert
Licensed User
Longtime User
Take a look at this source file.

I don't think you need to have a separate class, just pass the instance of the library object and include the methods in that. If for some reason that I can't immediately see you did need it you could declare is as a static class in your JSInterface source file so you wouldn't need a separate source file for the actual functions.

EDIT:- I notice I missed out a (String) cast on the return of ba.raiseEvent so the compile will complain.
 

Attachments

  • JSInterface2.zip
    689 bytes · Views: 432
Top