15

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?

cottonBallPaws
  • 21,220
  • 37
  • 123
  • 171
  • try calling this after you loadUrl(); ((InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); – zeitue Feb 16 '12 at 00:34
  • Hmmmm, so close. The keyboard closes and then reopens again real quick with this. Looks pretty weird ;). Though I might experiment with this idea more. – cottonBallPaws Feb 16 '12 at 00:47
  • Another part of the problem is that clearTextEntry() also remove's the EditText that the keyboard is focused on. – cottonBallPaws Feb 16 '12 at 01:02
  • setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); – zeitue Feb 16 '12 at 01:02
  • EditText.setText(""); that how I clear text also if you only have one thing on screen you want to have focus on you set the rest, in the XML to android:focusable="false" – zeitue Feb 16 '12 at 01:06
  • The problem is that the text view is a package level class called WebTextView. There is no way to access it, other than going through the WebView's children, and even then, the WebView is controlling it and may remove it during the clearTextEntry() call. – cottonBallPaws Feb 16 '12 at 18:57
  • I think I found something that might work, will report back. – cottonBallPaws Feb 16 '12 at 19:09
  • @AlokKulkarni, sorry I never found a good solution for this. The only possibility I thought of was if you queued your javascript calls in Java and then in the WebView's javascript, had a function on an interval that would call up to Java through an javascript interface and get the pending commands and then execute them. That way loadUrl would never be called. It is ugly but it might work. – cottonBallPaws Jul 31 '12 at 16:30
  • Thanks for the feedback littleFluffyKitty, i had thought about this , but i am targetting a solution on Android 2.3, and Javascript callbacks to Java break on 2.3 as you already might be knowing . I want to run a script after every second until the page loads completely. Seems that i will have to block user from taking any action on webview before entire page is loaded. – Alok Kulkarni Aug 01 '12 at 10:23
  • @AlokKulkarni I haven't had any issues with java <> javascript in 2.3 nor have I heard reports from my users. What are you seeing? – cottonBallPaws Aug 02 '12 at 00:19
  • I meant this issue (The javascript to java bridge on 2.3 Gingerbread is causing crashes. This is 100% reproducible using the WebViewDemo application ):- https://code.google.com/p/android/issues/detail?id=12987 .Can you please provide ,if you have any working code or just a skeleton code for what you are proposing to me ? – Alok Kulkarni Aug 02 '12 at 08:41
  • @AlokKulkarni hmm interesting, I haven't seen that error too often. I'll look more into it. As for an example workaround, I posted some code: http://stackoverflow.com/a/11784269/445348 – cottonBallPaws Aug 02 '12 at 19:16
  • I suffered this issue recently on Android 4.1.2 (on SAMSUNG tablet). Is there any chance that this problem has been solved in more recent versions? – opalenzuela Sep 16 '13 at 08:47

3 Answers3

12

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.

cottonBallPaws
  • 21,220
  • 37
  • 123
  • 171
  • Thanks for the snippet, really appreciate your effort. I have checked calling Javascript Interface method from Js on 2.3 and its breaking surely. So i fear that this might not work on 2.3. But i will check on other 2.x platforms whether its working or not. Thanks. – Alok Kulkarni Aug 03 '12 at 06:29
  • 1
    This will not work for event driven applications. Think about a chat/SMS/telephony apps. While you are typing, java layer needs to notify the UI with an event. You can't just pull it from JavaScript always. – Nizzy Feb 12 '13 at 06:22
  • I tried to implement your method using **Eval(jsString)** function in js. But it causes focus lose problem any ideas? – Dev.Sinto Jul 11 '13 at 06:10
  • http://stackoverflow.com/questions/15067853/alternative-way-for-communication-between-webview-and-native -> it works. – o0omycomputero0o May 12 '16 at 07:58
0

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.

BuvinJ
  • 10,221
  • 5
  • 83
  • 96
0

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);
}
martinstoeckli
  • 23,430
  • 6
  • 56
  • 87