I'm developing an Android application that contains a WebView.
I am trying to override onKeyDown activity's method.
I want to call a javascript method everytime phone's backButton is pressed and if the method is undefined I want to execute other native instruction (I want to close the activity).
This is the sense: natively I do this
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
if (true) {
webView.evaluateJavascript( "try{pageGoBack();}" +
"catch{window.JSInterface.noBackFunction();}",null);
webView.clearCache(true);
}
return true;
}
}
return super.onKeyDown(keyCode, event);
}
And for every jsp server side I want to implement something like this:
<script>
function pageGoBack() {
---do something to go back---
}
}
</script>
...
This method had to have the same name "pageGoBack" for every jsp but can have different inner code
That's the fact: before I load one of that jsp with the pageGoBack method everything works (error 'undefined' is catched and JSInterface method is executed).
When I load one of that page the method "remains in cache (???)" and it is always executed (also in those page without it) .
(Look that I also added webView.clearCache(true); but the problem remains)
Thank you -
[SOLVED]
That was not a problem of Android's WebView or cache. That's a problem of javascript.
When a function is defined in as "global" it assumes the global scope when it is evaluated for the first time. So when I visited the .jsp which contains pageGoBack() for the first time the function became globally visible.
Solution for my case:
I have lots of jsp with graphic component like this ◄ that means "go back to the previous page". I needed to associate the -jquery button tap function of the current page- to the Android back button.
So I:
Created an empty css class .empty-class-for-back-button
Gave this class to all those ◄ component
Made a global js function like this
function pageGoBack() {
var backBtn = $('.empty-class-for-back-button');
if (backBtn.length > 0){
backBtn.tap();
}
else{
throw "css class not found";
}
}
Android code is the same I've written in the question
Exception is now thrown correctly if there is no ◄ element in the current page.
Hope this can be helpful to somebody.
Related
I just started playing around with Blazor and I can already see the great potential of this new framework.
I'm wondering, though, how it will handle doing simple things like setting focus on an input control? For instance, after I handle a click event, I want to set the focus to a text input control. Do I have to use JQuery for something like that, or will Blazor have some built-in methods for that sort of thing?
Thanks
Update: I posted an answer below with an example of how to set the focus to a control by invoking a JavaScript function from the .Net code.
As of right now (Blazor 0.9.0) you create your JavaScript functions in the Index.html (or reference them from Index.html) and then in your Blazor page or component you call JsRuntime.InvokeAsync("functionName", parms);
https://learn.microsoft.com/en-us/aspnet/core/razor-components/javascript-interop
Blazor is just the replacement (to be more precise "value addition") to JavaScript. It is a client-side only solution (but it might add some easy binding to ASP.NET in the future).
Still, it's completely based on HTML and CSS. C# is replacing the JS part using web assembly. So nothing has changed on how you access / modify HTML controls.
As of now (version 0.1.0) you have to rely on HTML DOM focus() Method to do what you intend to do (yes you have to use JavaScript as of now :( ).
// Not tested code
// This is JavaScript.
// Put this inside the index.html. Just below <script type="blazor-boot"></script>
<script>
Blazor.registerFunction('Focus', (controlId) => {
return document.getElementById(controlId).focus();
});
</script>
//and then wrap it for calls from .NET:
// This is C#
public static object Focus(string controlId)
{
return RegisteredFunction.Invoke<object>("Focus", controlId);
//object type is used since Invoke does not have a overload for void methods. Don't know why.
//this will return undefined according to js specs
}
For more information, you can refer to below.
If you want to improve the packaging of JS neatly, you can do something like this:
https://stackoverflow.com/a/49521216/476609
public class BlazorExtensionScripts : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
builder.OpenElement(0, "script");
builder.AddContent(1, "Blazor.registerFunction('Focus', (controlId) => { document.getElementById(controlId).focus(); });");
builder.CloseElement();
}
public static void Focus(string controlId)
{
RegisteredFunction.Invoke<object>("Focus", controlId);
}
}
then add this component to the root: (App.cshtml):
<BlazorExtensionScripts></BlazorExtensionScripts>
<Router AppAssembly=typeof(Program).Assembly />
I want to add a more up-to-date (as of 0.9.0) example of calling a JavaScript function to set the focus to another control after some event, like clicking on a button. This might be helpful for someone just starting out with Blazor (like me).
This example builds on the example code in the Blazor documentation "Build Your First Blazor Components App" at https://learn.microsoft.com/en-us/aspnet/core/tutorials/build-your-first-razor-components-app?view=aspnetcore-3.0
First, follow all the instructions in the documentation. When you have a working To-Do List page, then add the following:
At the bottom of Index.html, under wwwroot, and below the script tag that loads the webassembly.js, add the following script:
<script>
window.MySetFocus = (ctrl) => {
document.getElementById(ctrl).focus();
return true;
}
</script>
At the top of your todo.cshtml page, add the following using statement:
#inject IJSRuntime JsRuntime;
In the #functions section of your todo.cshtml page, add the following function:
async void Focus(string controlId)
{
var obj = JsRuntime.InvokeAsync<string>(
"MySetFocus", controlId);
}
In the AddToDo() function, just below the line where you set the "newToDo" variable to an empty string, add a call to the Focus function, passing in the string id of the input control. (The example in the docs does not assign an ID to the input control, so just add one yourself. I named mine "todoItem").
void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
Focus("todoItem"); // this is the new code
}
}
Build and run your app. When you click the add new item button, the new item should be added to the list, the input control blanked out, and the focus should be back in the input control, ready for another item to be added.
From .NET 5 Preview 8
Set UI focus in Blazor apps
Blazor now has a FocusAsync convenience method on ElementReference for setting the UI focus on that element.
<button #onclick="() => textInput.FocusAsync()">Set focus</button>
<input #ref="textInput"/>
You can't directly call JavaScript function. You are required to first register your functions like,
<script>
Blazor.registerFunction('ShowControl', (item) => {
var txtInput = document.getElementById("txtValue");
txtInput.style.display = "";
txtInput.value = item;
txtInput.focus();
});
return true;
</script>
Then you need to declare a method in C# which calls this JavaScript function. Like,
private void CallJavaScript()
{
RegisteredFunction.Invoke<bool>("ShowControl", itemName);
}
You can call this C# method on click of button. Like,
<button id="btnShow" class="btn btn-primary" #onclick(CallJavaScript)>Show</button>
This post Create a CRUD App using Blazor and ASP.NET Core
shows a working demo of calling JavaScript from Blazor.
I am currently trying to port an existing Android application that we have to an IOS app. The issue here is that a large portion of our Android app was made using a lot of webviews with custom Javascript code called from Android.
If anyone is not familiar with it, the code for android would go something like this:
mWebView.addJavascriptInterface(WebInterface(this), "Android");
// more settings or whatever
mWebView.loadUrl("yourUrl.html.whatever");
Then the interface to the Javascript code reads like this:
public class WebInterface {
Context mContext;
public WebInterface(Context c) { this.mContext = c;}
#JavascriptInterface
public String returnMessage() {
Toast.makeText(mContext, "This is being called from the interface", Toast.LENGTH_SHORT).show();
return "This is being called from the interface";
}
}
At this point any time we want to access the code for the returnMessage() method inside of the webview we simply call:
var getContentsFromIntefrace = Android.returnMessage()
// getcontentsFromInterface is now "This is being called from the interface"
These targeted calls(Android.returnMessage()) allow me to call the interface code only when I need to do so, in contrast, if I was to use Android API´s evaluate javascript methods I would have to watch for the code executing all over the place e.g if my evaluatejavascript Android API method sets a variable to something it will do so in any other part of the code where said variable exist(please do correct me if I am wrong, it seems fuzzy at first but it is an issue thus far)
===== The issue I am having with IOS
The above is merely an explanation of what I need to do, to show what I am attempting on IOS I will demonstrate the code:
In IOS if I want to se functions that call on swift code inside my controller I have to set it in JS as:
function testButton() {
webkit.messageHandlers.callbackHandlerTestSend.postMessage({action: "testButtonAction", data: {name: "E.A.P"}});
}
The above sends a named message('callbackHandlerTestSend') with the key value pairs of action and data which can be parsed on the appropriate swift delegate as:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if (message.name == "callbackHandlerTestSend") {
guard let body = message.body as? [String:Any] else {
print("Unable to do connection")
return
}
for(key, value) in body {
print("\(key) : \(value)")
if(key == "action"){
if(value as! String == "testButtonAction") {
intCounter += 1
print("The testButtonAction message action has been called")
webView.evaluateJavaScript("receiverP.innerHTML = 'It works \(intCounter)'", completionHandler: nil)
webView.evaluateJavaScript("receiverFunction('Message test')", completionHandler: nil)
// FIXME: evaluate JS might call on any other instance of receiverP, this is an issue
}
}
}
}// body of if
}// end of delegate
This is currently the only way in which I have been able to send Swift code back to the webview, by injecting the Javascript code with webView.evaluateJavascript()
My problem is that I need to capture everything with the whole:
webkit.messageHandlers.callbackHandlerTestSend.postMessage({action: "testButtonAction", data: {name: "E.A.P"}});
And it becomes bothersome to continue to do it as well as to have to individually parse each call for the proper parameters to be set while at the same time trying to not inject javascript code without messing something else up. So calling javascript and sending javascript code seems extremely cumbersome at this point and I would like to know if there is something I could do for the code to look more like the Android counterpart.
Any advice on what cool external libs out there I could use or in how could I modify the code to this would be greatly appreciated, I have been trying to find something in the IOS docs but have not found anything that I could use.
-- Edit:
At this time I am seriously considering an alternative such as React Native since it seems as the fastest route to develop an application that requires heavy usage of web like content as well as having limited knowledge of IOS in contrast to Android. I would still want to know if there is a viable solution to the above.
I have a WebView in my app that displays a page not belonging to me. My desired behavior is, if any link is tapped by the user, the device's Browser app is launched, and the resulting page is loaded there. Unfortunately, this page is doing some weird things, so shouldOverrideUrlLoading() does not fire.
My attempted solution is to hook some javascript into pushState and use an interface to run Android code to launch the browser app.
Here is my interface:
public class LaunchExternalBrowserHack {
Context mContext;
LaunchExternalBrowserHack(Context c) {
mContext = c;
}
#JavascriptInterface
public void launchExternalBrowser(String url) {
openUrl(url);
}
}
I'm injecting some javascript into the page in onPageFinished():
public void onPageFinished(WebView view, String url) {
mWebView.loadUrl(javascript);
}
Here is my javascript:
private final String javascript = "javascript:history.pushState = function (state, title, url) { console.log(url); console.log(location.href); Uphoria.launchExternalBrowser(location.origin, url); };";
And, of course, I'm adding the interface to the WebView:
mWebView.addJavascriptInterface(new LaunchExternalBrowserHack(getContext()), "Android");
So this seems to work. The Browser app is launching and the next page is opening.
However, the WebView is also moving forward, too. I want to prevent this, but I cannot find a way to prevent the WebView moving forward while still allowing me to capture the forwarding url and launch the Browser app. As I mentioned earlier, with this webpage, shouldOverrideUrlLoading is not firing.
Ideas?
The user is trying to move away from the page. to stop him from moving out, return false. to let him move away, don't return anything but you can execute any code that you may want. This event is fired immediately when navigation event occurs including page refresh.
private final String javascript = "window.onbeforeunload = function(){ dosomething(); }; window.unload = function(){ dosomething(); }; function dosomething() { /* Write code here */ };"
You need to include the entire javascript code above, since some browsers use onbeforeunload and some use unload
EDIT
After careful reading of your problem I found my solution to be of little use.
Here's another approach using history. If your history is blank, that means its the first page. but if you have some items in history, it means that navigation has occurred.
You can check it as following
private final String javascript = "javascript:"+"if(window.history.length>=1 && document.referrer=='') Android.launchExternalBrowser(location.href);";
This answer is wrong. Kept here because it has importantly relevant comments.
The reason that you are not getting a result from your Javascript, is that you are not calling a function directly. You are assigning a function in your Javascript that returns a value, but it will not return that to your code, it will return that to whatever calls history.pushState().
private final String javascript = "javascript:" + "var pushState = history.pushState; history.pushState = function () " +
"{ pushState.apply(history, arguments); console.log(arguments); return location.href; " +
"};";
Instead, you can test whether something like this returns you a value:
private final String javascript = "(function() { return location.href; })();";
I´m using code splitting in GWT to reduce the size of the initial JavaScript.
While my application initializes, I want to prefetch the other (bigger) part of my code as explained in the docs (www.gwtproject.org/doc/latest/DevGuideCodeSplitting.html).
private void doSth(final boolean prefetch) {
GWT.runAsync(new RunAsyncCallback() {
public void onFailure(Throwable caught) {
Log.error("Loading the code failed!");
}
public void onSuccess() {
if(prefetch)
return; //do nothing. just a prefetch
//here is the loaded code
}
});
}
But I cannot recognize a performance improvement. As I analyzed the browser logs, I recognized, that the request for loading the JavaScript is not marked as XHR. Does GWT load the code of a split point synchronously?
The performance improvement is in the initial downloaded code, assuming nothing else references that code. If anything else does the work of //here is the loaded code, then there will be either very little or no code to break out into a separately downloaded JS file.
This feature can be disabled in several ways, including by using dev mode or setting a compiler flag to skip this process. In this case, yes, the split point runs synchronously, since it makes no sense to wait. Additionally, after the file has been loaded once, it does not need to be loaded the next time the code is invoked within the same page load.
If your server is set to cache correctly, then after the first visit the savings is even smaller since there is no download to do - you only save the time taken to parse that code into the browser's JS VM.
But beyond that, we're going to need more information.
Here's a quick demo showing how the split point can be written with a little more meat to it, and letting you use your browser to notice how the split point code was brought in separately.
public class SampleEntryPoint implements EntryPoint {
public void onModuleLoad() {
Label label = new Label("Hello, World!");
label.addClickHandler(new ClickHandler() {
#Override
public void onClick(ClickEvent event) {
GWT.runAsync(new RunAsyncCallback() {
public void onFailure(Throwable var1) {/*ignore*/}
public void onSuccess() {
Window.alert("Clicked, and loaded in split point!");
}
});
}
});
RootPanel.get().add(label);
}
}
Code and sample:
https://viola.colinalworth.com/proj/755e224e7f48a047703d44eb6903d926/project/client/SampleEntryPoint.java
Standalone sample:
https://viola.colinalworth.com:444/compiled/755e224e7f48a047703d44eb6903f76c/
When you load this page, the nocache file loads, as does the initial download (as seen through Chrome's inspector's Network tab):
Then, when you click the Label widget, the onClick fires which triggers the runAsync and downloads the extra split point (plus the "leftover" fragment):
After those two new entries have been added to your Network tab, you see the alert message appear. Subsequent clicks do not result in this slight delay, nor do they force this extra JS to download again.
Also note that these are not loaded as AJAX/XHR calls, but as a script tag to be added to the page. Clicking on the details in the Initiator column (not pictured) leads to this (formatted for readability):
function fb(a) {
var b, c, d;
d = (bb(), window);
b = d.document;
c = b.createElement('script');
(!!a.a || a.b) && cb(c, a.a, a.b);
eb(c, a.c);
(b.head || b.getElementsByTagName('head')[0]).appendChild(c);
return c
}
Getting through the obfuscated code, we see that a <script> tag is created, and appended to the <head> of the page.
Digging in deeper, we can find that the AsyncFragmentLoader.LoadingStrategy interface describes how to go get this fragment, and that com/google/gwt/core/AsyncFragmentLoader.gwt.xml wires this by default to XhrLoadingStrategy. However, both the xs and xsiframe linkers change this, to CrossSiteLoadingStrategy and ScriptTagLoadingStrategy respectively. And as of recent versions of GWT (you didn't specify, so I'm assuming you are using a recent version), the xsiframe linker is the default. From Core.gwt.xml:
<add-linker name="xsiframe" />
We can customize this by switching to the old linker, or just replacing the strategy. Note that switching the an XHR strategy will prevent cross-domain loading from working correctly (such as SuperDevMode), so be careful with this.
Much as AsyncFragmentLoader.gwt.xml wired the interface to XhrLoadingStrategy, and CrossSiteIframeLinker.gwt.xml changed it to ScriptTagLoadingStrategy, we can change it back. We create a rule that replaces LoadingStrategy with XhrLoadingStrategy, and list it after our GWT inherits statements in our .gwt.xml file:
<replace-with class="com.google.gwt.core.client.impl.XhrLoadingStrategy">
<when-type-is class="com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy" />
</replace-with>
This is what the old default used to rely on as part of the std linker (com.google.gwt.core.linker.IFrameLinker), though this is no longer encouraged and may be removed in a later release.
Since the way you call javascript on a WebView is through loadUrl("javascript: ... "); The keyboard cannot stay open.
The loadUrl() method calls loadUrlImpl() , which calls a method called clearHelpers() which then calls clearTextEntry(), which then calls hideSoftKeyboard() and then we become oh so lonely as the keyboard goes away.
As far as I can see all of those are private and cannot be overridden.
Has anyone found a workaround for this? Is there a way to force the keyboard to stay open or to call the javascript directly without going through loadUrl()?
Is there anyway to override the WebView in a way to prevent (the private method) clearTextEntry() from being called?
Update
KitKat added a public method for invoking javascript directly: evaluateJavascript()
For older apis, you could try a solution like below, but if I had to do this again I'd look at just building an compatibility method that on KitKat uses the above method and on older devices, uses reflection to drill down to a inner private method: BrowserFrame.stringByEvaluatingJavaScriptFromString()
Then you could call javascript directly without having to deal with loadUrl and adding "javascript: " to the script.
Old Answer
As requested by Alok Kulkarni, I'll give a rough overview of a possible workaround I thought of for this. I haven't actually tried it but in theory it should work. This code is going to be rough and is just to serve as an example.
Instead of sending the calls down through loadUrl(), you queue your javascript calls and then have javascript pull them down. Some thing like:
private final Object LOCK = new Object();
private StringBuilder mPendingJS;
public void execJS(String js) {
synchronized(LOCK) {
if (mPendingJS == null) {
mPendingJS = new StringBuilder();
mPendingJS.append("javascript: ");
}
mPendingJS
.append(js)
.append("; ");
}
}
Instead of calling loadUrl() call that method. (For making this simple I used a synchronized block, but this might be better suited to a different route. Since javascript runs on its own thread, this will need to be thread safe in some way or another).
Then your WebView would have an interface like this:
public class JSInterface {
public String getPendingJS() {
synchronized(LOCK) {
String pendingCommands = mPendingJS.toString();
mPendingJS.setLength(0);
mPendingJS.append("javascript: ");
return pendingCommands;
}
}
}
That returns a String with the pending commands and clears them so they don't get returned again.
You would add it to the WebView like this:
mWebView.addJavascriptInterface(new JSInterface(), "JSInterface");
Then in your javascript you would set some interval in which to flush the pending commands. On each interval it would call JSInterface.getPendingJS() which would return a String of all of the pending commands and then you could execute them.
You could further improve this by adding a check in the execJS method to see if a EditText field exists in the WebView and is in focus. If there is one, then you would use this queueing method, but if there wasn't one in focus then you could just call loadUrl() like normal. That way it only uses this workaround when it actually needs to.
Regarding older APIs (pre 19), I used a similar method to the excepted answer, but slightly different.
First, I keep track of if the keyboard is displayed by using javascript in the webview roughly like so:
document.addEventListener( "focus", function(e){
var el = e.target;
reportKeyboardDisplayedToJava( isInputElement( el ) );
}, true);
document.addEventListener( "blur", function(e){
reportKeyboardDisplayedToJava( false );
}, true);
If the keyboard is displayed, and a js injection is attempted by the Android Java layer – I “defer” that injection. I add it to a string list, allow the user to finish up their input, and then upon the keyboard disappearing, I detect that and execute the backlog of injections.
I could implement cottonBallPaws's idea to use the internals of WebView with reflection, and got it to work for my 4.2 device. There are gracious fallbacks for Android versions older than KitKat.
The code is written in Xamarin, but it should be easily adaptable to native Java code.
/// <summary>
/// Executes a JavaScript on an Android WebView. This method offers fallbacks for older
/// Android versions, to avoid closing of the soft keyboard when executing JavaScript.
/// </summary>
/// <param name="webView">The WebView to run the JavaScript.</param>
/// <param name="script">The JavaScript code.</param>
private static void ExecuteJavaScript(Android.Webkit.WebView webView, string script)
{
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat)
{
// Best way for Android level 19 and above
webView.EvaluateJavascript(script, null);
}
else
{
try
{
// Try to do with reflection
CompatExecuteJavaScript(webView, script);
}
catch (Exception)
{
// Fallback to old way, which closes any open soft keyboard
webView.LoadUrl("javascript:" + script);
}
}
}
private static void CompatExecuteJavaScript(Android.Webkit.WebView androidWebView, string script)
{
Java.Lang.Class webViewClass = Java.Lang.Class.FromType(typeof(Android.Webkit.WebView));
Java.Lang.Reflect.Field providerField = webViewClass.GetDeclaredField("mProvider");
providerField.Accessible = true;
Java.Lang.Object webViewProvider = providerField.Get(androidWebView);
Java.Lang.Reflect.Field webViewCoreField = webViewProvider.Class.GetDeclaredField("mWebViewCore");
webViewCoreField.Accessible = true;
Java.Lang.Object mWebViewCore = webViewCoreField.Get(webViewProvider);
Java.Lang.Reflect.Method sendMessageMethod = mWebViewCore.Class.GetDeclaredMethod(
"sendMessage", Java.Lang.Class.FromType(typeof(Message)));
sendMessageMethod.Accessible = true;
Java.Lang.String javaScript = new Java.Lang.String(script);
Message javaScriptCodeMsg = Message.Obtain(null, 194, javaScript);
sendMessageMethod.Invoke(mWebViewCore, javaScriptCodeMsg);
}