Call JavaScript from C# object in Xamarin - javascript

I have been able to call JavaScript from C# inside the MainActivity but I'm trying to do so from an object. The majority of my app runs inside a WebView, my JavaScript calls to my C# Interface invoking an asynchronous function and when it's complete I would like to call back to my JavaScript but am unable to do so. Here is my current setup:
In my MainActivity I setup my WebView as such:
browser = FindViewById<WebView>(Resource.Id.mainView);
browser.SetInitialScale(1);
browser.SetWebChromeClient(new GeoWebChromeClient());
browser.Settings.UseWideViewPort = true;
browser.Settings.LoadWithOverviewMode = true;
if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
{
WebView.SetWebContentsDebuggingEnabled(true);
}
browser.Settings.SetGeolocationEnabled(true);
browser.Settings.JavaScriptEnabled = true;
browser.AddJavascriptInterface(new JSCSMedium(this, ref browser), "Android");
browser.LoadUrl("file:///android_asset/web/index.html");
Then inside the JSCSMedium object I have an asynch function:
[Export]
[JavascriptInterface]
public void SyncApps()
{
Task t = Task.Run(() => {
IList<ApplicationInfo> tempApps = Application.Context.PackageManager.GetInstalledApplications(PackageInfoFlags.MatchDirectBootAware);
string packageName = "";
string appName = "";
for (int i = 0; i < tempApps.Count(); i++)
{
packageName = tempApps[i].PackageName;
appName = tempApps[i].LoadLabel(manager);
var root = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
var filePath = System.IO.Path.Combine(root, "system");
filePath = System.IO.Path.Combine(filePath, packageName);
if (!System.IO.Directory.Exists(filePath))
{
System.IO.Directory.CreateDirectory(filePath);
}
filePath = System.IO.Path.Combine(filePath, "icon.png");
if (!System.IO.File.Exists(filePath))
{
Drawable icon = tempApps[i].LoadIcon(Application.Context.PackageManager);
BitmapDrawable bd = (BitmapDrawable)icon;
CreateAppIcon(bd.Bitmap, packageName);
}
Intent intent = Application.Context.PackageManager.GetLaunchIntentForPackage(packageName);
if (intent != null)
{
apps.Add(tempApps[i]);
}
}
});
}
If I don't do the C# as an async function it runs and returns data fine, but this process takes a bit of time and blocks the app temporarily. Inside my MainActivity I can call JavaScript just fine with:
browser.EvaluateJavascript("javascript: alert('fooBar');", null);
But browser is not accessible inside the JSCSMedium. I've tried passing the browser object as a reference and normally but it throws an exception stating that the EvaluateJavascript function must be called on the same thread as where it was instantiated. I've also tried sending a reference of my MainActivity to the JSCSMedium and call a function inside the MainActivity to run the EvaluateJavascript but it seems to do nothing. No error, not crash, just nothing.

The problem is Task.Run forces the code to run in the thread pool, and browser.EvaluateJavascript needs to run on the main thread.
You have at least two options here, depending on your needs:
1) Run the EvaluateJavascript call inside the Task.Run block with something like:
var h = new Handler(Looper.MainLooper);
h.Post(() => browser.EvaluateJavascript("javascript: alert('fooBar');", null));
2) Run the EvaluateJavascript call outside the Task.Run block:
[Export]
[JavascriptInterface]
public async void SyncApps()
{
await Task.Run(() => {
//...
});
browser.EvaluateJavascript("javascript: alert('fooBar');", null);
}
Not really sure if you can change the return type of SyncApps(). If JS doesn't complain, you better change that too.

Related

Why does electron gives access violation trying to access a new Isolate?

I wrote a Node.js addon to be used in Electron framework
The main entry of the addon calls another C++ library that makes a long running operation so I put callbacks in to have reports of the operation progress.
So the C++ library calls a callback in my addon but the Isolate is null so I tried to create a new one
The isolate is created well but when I try to use it to have new Local i had this error:
Exception thrown at 0x0000000000000000 in electron.exe: 0xC0000005: Access violation executing location 0x0000000000000000.
Here is an excerpt of my code (the progress_cb variable takes value in the main entry of the addon casting the function passed in Javascript call):
Local<Function> progress_cb;
class ArrayBufferAllocator : public ArrayBuffer::Allocator {
public:
virtual void* Allocate(size_t length) {
void* data = AllocateUninitialized(length);
return data == NULL ? data : memset(data, 0, length);
}
virtual void* AllocateUninitialized(size_t length) { return malloc(length); }
virtual void Free(void* data, size_t) { free(data); }
};
void progress_callback(int read, int write, int total)
{
V8::Initialize();
ArrayBufferAllocator allocator;
Isolate::CreateParams params;
params.array_buffer_allocator = &allocator;
Isolate* isolate = Isolate::GetCurrent();
if (!isolate) {
isolate = Isolate::New(params);
isolate->Enter();
}
const unsigned argc = 3;
Local<Integer> r = Int32::New(isolate, (int32_t)read);
Local<Integer> w = Int32::New(isolate, (int32_t)write);
Local<Value> t = Int32::New(isolate, (int32_t)total);
Local<Value> argv[argc] = { r, w, t };
progress_cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
}
Any suggestion?

JXBrowser JSFunctionCallback and IFrame

I'm porting my jxbrowser integrations from 4.9x to 6.11.x, and i've got a problem with the JavaScript - JavaBridge with The IFrame. I register correctly with the follow code, and work as expect when I call the function from js in the main page. But doesn't work when the function is called from js inside the iframe.
browser.addScriptContextListener(new ScriptContextAdapter() {
#Override
public void onScriptContextCreated(ScriptContextEvent event) {
final Browser browser = event.getBrowser();
JSValue window = browser.executeJavaScriptAndReturnValue("window");
window.asObject().setProperty("resolveMsg", new JSFunctionCallback() {
#Override
public Object invoke(Object... params) {
String msgId = (String) params[0];
Builder builder = WrLocMsg.builder(msgId);
return builder.buildOriginalMessage();
}
});
}
});
Resolved by recovery window in this way:
JSValue window = browser.executeJavaScriptAndReturnValue(event.getJSContext().getFrameId(),"window");

Background thread brakes upcall from JavaScript to JavaFX

After migrating JRE version 1.8.0_66 to 1.8.0_111 I've encountered an issue with making upcall from JavaScript to JavaFX.
Long story short: while there is a running background thread, WebView/WebEngine refuses to make JS-to-Java calls.
I use WebView to render HTML content which is generated from a domain Data Model (DM). Content contains elements with a handler assigned to it as follows:
<a href='#' onclick='explainHeadWord(this)'>some_word</a>
JS part looks like:
function explainHeadWord(hwElement) {
jsBridge.jsHandleQuery(hwElement.innerHTML);
}
function testBridge() {
jsBridge.jsTest();
}
where jsBridge is an inner Java class of the Controller
public class JSBridge {
public void jsHandleQuery(String headWord) {
log("jsBridge: jsHandleQuery: requested %s", headWord);
handleQuery(headWord);
}
public void jsTest() {
log("jsBridge: jsTest: test succeeded ");
}
}
which is injected as follows:
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
engine.setJavaScriptEnabled(true);
JSObject window = (JSObject) engine.executeScript("window");
window.setMember("jsBridge", new JSBridge());
//engine.executeScript("jsTest()");
//engine.executeScript("explainHeadWord(document.getElementsByTagName('a')[0])");
//engine.executeScript("jsBridge.jsHandleQuery(document.getElementsByTagName('a')[0])");
}
Besides the main DM I have an Index of cross-references which is Map<String, Collection<String>> built from DM, and a trigger method rebuilding that Index in the background each time DM changes. The first approach (which is worked fine on version 1.8.0_66) was based on ExecutorService:
private ExecutorService executor = Executors.newCachedThreadPool();
private Future<Boolean> indexer = executor.submit(() -> false);
...
private void rebuildIndex() {
executor.submit(() -> {
indexer.cancel(true);
indexer = executor.submit(() -> {
fullSearchIndex = getIndex();
if (isIndexingAborted()) return false;
return true;
});
try {
if (indexer.get()) {
log("resetIndex: Done");
updateTableView();
}
} catch (InterruptedException e) {
...
}
});
}
As was expected, clicking on an anchor in a WebView resulted to a JS-call jsBridge.jsHandleQuery(hwElement.innerHTML) and eventually to handleQuery(headWord) method call implemented in Controller. But after migrating JRE to version 1.8.0_111 WebView stopped to respond to an anchor clicking.
I've investigated logs and found that injecting jsBridge was successful as well as executing test scripts commented in the code below line window.setMember(). Clicking on an <a> element led to nothing. But without running test scripts (commented) there were records appeared in the log:
<record>
<date>2017-01-09T02:00:47</date>
<millis>1483920047169</millis>
<sequence>160</sequence>
<logger>com.sun.webkit.WebPage</logger>
<level>FINE</level>
<class>com.sun.webkit.WebPage</class>
<method>fwkAddMessageToConsole</method>
<thread>11</thread>
<message>fwkAddMessageToConsole(): message = TypeError: jsBridge.jsHandleQuery is not a function. (In 'jsBridge.jsHandleQuery(hwElement.innerHTML)', 'jsBridge.jsHandleQuery' is undefined), lineNumber = 26, sourceId = jar:file:/.../jar.jar!/view.js</message>
</record>
And after a moment the background (indexing) thread was complete and a content in the WebView was reloaded clicking on <a> elements starts to respond again - jsBridge.jsHandleQuery was executed.
The indexing thread executes getIndex() method that traverses DM and returns collected into Map data from DM. There's niether any interaction with FX application thread nor WebView depends on Index. Substituting fullSearchIndex = getMockIndex();
private Map<String, Collection<String>> getMockIndex() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) { }
return Collections.emptyMap();
}
in background thread doesn't change <a>'s behavior.
Next step was to refactor background thread into FX style by utilizing
javafx.concurrent.Service but the result is the same.
Will appreciate for pointing at what do I do wrong and how to tackle this issue.
Try to instantiate Bridge outside of the listener, i.e.,
final JSBridge bridge = new JSBridge();
engine.setJavaScriptEnabled(true);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
JSObject window = (JSObject) engine.executeScript("window");
window.setMember("jsBridge", bridge);
}
});
(Worked for me migrating from 1.8.0_91 to 1.8.0_121).

Wicket AbstractDefaultAjaxBehavior do recursive update the page

I have some ajax Behaviour that should pick some data using JS, and turn it back to Java. Sometimes it works but quite ofen it is just add url parameter and do page refresing/
public abstract class LoggedVKIdBehaviour extends AbstractDefaultAjaxBehavior {
private static final Logger logger = LoggerFactory.getLogger(LoggedVKIdBehaviour.class);
#Override
protected void respond(AjaxRequestTarget target) {
String loggedVkId = RequestCycle.get().getRequest().getRequestParameters().getParameterValue("logged_vkid").toString();
logger.info("ajax has comming with logged VK ID " + loggedVkId);
recived(target, loggedVkId);
}
protected abstract void recived(AjaxRequestTarget target, String loggedVkId);
#Override
public void renderHead(final Component component, IHeaderResponse response) {
super.renderHead(component, response);
Map<String, Object> map = new HashMap<>();
map.put("callbackFunction", getCallbackFunction(CallbackParameter.explicit("logged_vkid")));
//
PackageTextTemplate ptt = new PackageTextTemplate(LoggedVKIdBehaviour.class, "vkid_callback.js");
OnDomReadyHeaderItem onDomReadyHeaderItem = OnDomReadyHeaderItem.forScript(ptt.asString(map));
response.render(onDomReadyHeaderItem);
}
}
js template
var calback = ${callbackFunction};
var logged_vk_id = 11;
function authInfo(response) {
if (response.session) {
logged_vk_id = response.session.mid;
calback(response.session.mid);
console.log("recived callback from VK " + logged_vk_id);
}
}
$(document).ready(function () {
VK.Auth.getLoginStatus(authInfo);
});
it is do recursive redirection like http://localhost:8080/mytool/product/1?logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332773...
As i understand Ajaj technology - iti asynchronus requests, that shouldn't touch main url at all. So what is the reason for page refreshing?
this is generated Callback function
function (logged_vkid) {
var attrs = {"u":"../wicket/bookmarkable/com.tac.kulik.pages.product.ProductPage?12-1.IBehaviorListener.0-&productID=1"};
var params = [{"name":"logged_vkid","value":logged_vkid}];
attrs.ep = params.concat(attrs.ep || []);
Wicket.Ajax.ajax(attrs);
}
I use wicket 7.2
I did a lot investigations for few days. And found that when i remove
setPageManagerProvider(new NoSerializationPageManagerProvider(this));
Application throw me exepton in polite logs
org.apache.wicket.WicketRuntimeException: A problem occurred while
trying to collect debug information about not serializable object look
like it is could come from aused by: java.io.NotSerializableException:
com.tac.kulik.panel.smaccounts.SMAccountsPanel$1
which means that page tryed to be serialized for SOME REASON but $1 it is mean Anonimous class. I had few class created anonimously to ges some ajax links coming from ListView to be managed on parent panel. So After removing this Anonimous class logic, everything start and run well.
So i am happy, but still don't understand which reason page did serialization after ajax, and what the reason was to refresh whole page.

Invoke JS method from C# using SignalR?

I know there are lots of examples out there to do with SignalR but I can't seem to get it working, I was hoping that one of you may be able to show (in full) how a WebPage (threaded loop so we can see it happening over and over) could call a JS method on a Page and change a text label or create a popup or, just something so that we an see the method execute?
I'll give you my code and maybe you can point out the error, but any basic example of Server->Client invocation without a Client first making a request would be amazing!
Hub:
[HubName("chat")]
public class Chat : Hub
{
public void Send(string message)
{
// Call the addMessage method on all clients?
Clients.addMessage(message);
}
}
Calling (Threaded) method:
private void DoIt()
{
int i = 0;
while (true)
{
var hubContext = GlobalHost.ConnectionManager.GetHubContext<Chat>();
hubContext.Clients.addMessage("Doing it... " + i);
i++;
Thread.Sleep(500);
}
}
JS:
$(function () {
// Proxy created on the fly
var chat = $.connection.chat;
// Declare a function on the chat hub so the server can invoke it
chat.addMessage = function (message) {
confirm("Are you having fun?");
confirm(message);
};
// Start the connection
$.connection.hub.start();
});
The issue I had was a self closing JS import tag which stopped all JS on the page being run...
For others who have the same issue, here is my working example on a Server pushing data out to all clients without any prompting from a client:
Javascript:
$(function () {
// Proxy created on the fly
var chat = $.connection.chat;
// Declare a function so the hub can invoke it
chat.addMessage = function (message) {
document.getElementById('lblQuestion').innerHTML = message;
};
// Start the connection
$.connection.hub.start();
});
HTML:
<h2 id="lblQuestion" runat="server">Please wait for a question...</h2>
Hub:
[HubName("chat")]
public class Chat : Hub
{
public void Send(string message)
{
// Call the addMessage method on all clients
Clients.addMessage(message);
}
public void Broadcast(string message)
{
IHubContext context = GlobalHost.ConnectionManager.GetHubContext<Chat>();
context.Clients.addMessage(message);
}
}
Call to clients:
private void DoIt()
{
int i = 0;
while (true)
{
var hubContext = GlobalHost.ConnectionManager.GetHubContext<Chat>();
hubContext.Clients.addMessage("Doing it... " + i);
i++;
Thread.Sleep(500);
}
}
Threaded call to DoIt():
var thread = new Thread(new ThreadStart(DoIt));
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

Categories