Does WebView saveState() preserve Javascript variables/environment? - javascript

I've searched many threads and still cannot find the answer to my question. I'm working on an Android app which uses WebView.
I use onSaveInstanceState() and onRestoreInstanceState() to save the WebView state like this:
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
webView.saveState(savedInstanceState);
}
and
#Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
webView.restoreState(savedInstanceState);
}
I also have this in my onCreate():
public void onCreate(Bundle savedInstanceState) {
... other code ...
if(savedInstanceState != null){
webView.saveState(savedInstanceState);
}else{
webView.loadUrl("http://mypage");
}
}
The problem: It seems that restoring WebView does not restore the Javascript variables/environment/workspace at all. When app is killed in the background and then restored, the whole Javascript is gone with all the objects/variables. The javascript code is name-spaced, i.e. window.utilitiesPack, window.eventHandlers, window.automation, etc and they are undefined. Also JQuery and other javascript plugins are used: all of them seem to be undefined after restore state. Basically the whole Javascript is not restored.
Can someone confirm or disprove that this is how it really is (Javascript not saved)? If the whole Javascript workspace is not saved, then what exactly does WebView.saveState() save? Is there some easy/elegant way to use existing API to preserve the Javascript objects?
//======================================================
Update1: So the problem remains. The most significant difficulty is this:
I am launching a Camera Intent for a result. When picture is taken, app gets back to WebView activity and is supposed to use Javascript to update HTML5 LocalStorage with some data variables.
The main Activity with WebView gets killed while Camera Activity is displayed, so when we come back to WebView, there is no Javascript anymore and no functions I can call from Android code. This happens every single time on Galaxy S3. It still happens on other phones, but not every time on picture taking.
Don't know what to do here. Somehow I must make the main Activity with WebView to retain the state while the picture is being taken with the Camera Intent. Does anyone have an idea how this can be achieved?

As user2113581 commented, moving the WebView into a context of the application rather than the Activity is a potential solution. This still has all of the pitfalls that user2113581 mentioned, including <select> not working (because it creates a window, but an application context does not have a window token).
I have extended my Application to manage my webview...
MyApplication.java:
public class MyApplication extends Application {
private WebView mWebView;
private boolean mWebViewInitialized;
// ...
#Override public void onCreate() {
super.onCreate();
mWebView = new WebView(this);
mWebViewInitialized = false; // we do some initialization once in our activity.
}
public WebView getWebView() { return mWebView; }
public boolean isWebViewInitialized() { return mWebViewInitialized; }
public void setWebViewInitialized(boolean initialized) {
mWebViewInitialized = initialized;
}
}
In our activity:
#Override protected void onCreate(Bundle savedInstanceState) {
// ...
MyApplication app = (MyApplication) getApplication();
mWebView = app.getWebView();
if (!app.isWebViewInitialized()) {
/* first time initialization */
app.setWebViewInitialized(true);
}
}
Finally, in your Activity, you will want to add the mWebView to a container view (FrameLayout, or similar) during a lifecycle event that makes sense. You'll need to remove it from the container when the activity is being paused or stopped. I've used onResume and onPause with good results.

Very few information found, only this line:
Please note that this method no longer stores the display data for this WebView. The previous behavior could potentially leak files if restoreState(Bundle) was never called.
and this line:
Returns the same copy of the back/forward list used to save the state.
in the javadoc for WebView.saveState(Bundle).

Related

Trigger a function in an App via HTML / JS

I wanted to implement in-app purchases to my web app without adding a side menu.
I use the RocketWeb Application Template for the Webapp found on codecanyon.
Is it possible to implement a trigger in the App and on the Website to make this happen:
User presses Button1 (HTML) that triggers a function in the App, and an In-app Purchase Modal pops up.
I want to do that because Google isn't too happy about adding direct payments.
The Modals n Co should be no problem, but I'm wondering if it's possible to "Bridge" App to Website basically.
Yes, its possible. You can bridge the Webview to your App - JS in WebView
What you need to do:
Make sure JS is enabled in your WebView
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
Create JS listener interface, that can handle messages sent from the WebView:
public class WebAppInterface {
Context mContext;
/** Instantiate the interface and set the context */
WebAppInterface(Context c) {
mContext = c;
}
/** Show a toast from the web page */
#JavascriptInterface
public void openPurchaceModal(String toast) {
//TODO open your purchase screen using mContext as your context
}
}
Caution: If you've set your targetSdkVersion to 17 or higher, you must add the #JavascriptInterface annotation to any method that you want available to your JavaScript, and the method must be public. If you do not provide the annotation, the method is not accessible by your web page when running on Android 4.2 or higher.
Set your WebView's listener:
//Adds listener to the webView, under global JS object `Bridge` (You can use any name you want here)
webView.addJavascriptInterface(new WebAppInterface(this), "Bridge");
Use your Bridge object in JS inside the WebView, wherever you need it, like this:
Bridge.openPurchaceModal();
You can have multiple methods defined in your listener, and call them like:
Bridge.[method name]();
You also can have primitive arguments like String, int, boolean etc' to these methods:
...
#JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
...
And use it:
Bridge.showToast("toast text");

Closing a webpage running in a WPF WebBrowser

First a bit of background
I'm working on a Web application, that will be running within a WebBrowser, within a WPF application.
This is a temporary necessity while we're gradually moving functionality to the web app. As long as that's not finished, the WPF client is still needed. Ultimately the WPF client will phase out completely.
Now to the issue at hand
When the user closes the client (webpage), the webbrowser should catch that event and also close the window it is a child to.
I found this link describing what I would need: WebBrowser and javascript window.close()
Alas, I don't think the answer described there would still work, as it's not possible to even do a window.close(), because I'm not the one opening the window I'm running on. Browsers have (rightfully) tightened their security since then.
The question
Is there a way to trigger a Window close from the client, that bubbles up to the WPF?
Thanks.
I have used a WebBrowser control to call methods in a WPF application from the JavaScript before using WebBrowser.InvokeScript and WebBrowser.ObjectForScripting
See this MSDN article How to: Implement Two-Way Communication Between DHTML Code and Client Application Code
Also see this CodeProject article which looks like it might solve your problem
Call a C# Method From JavaScript Hosted in a WebBrowser
[ComVisible(true)]
public class ScriptManager
{
// Variable to store the form of type Form1.
private Window _window;
// Constructor.
public ScriptManager(Window window)
{
// Save the form so it can be referenced later.
_window = window;
}
// This method can be called from JavaScript.
public void MethodToCallFromScript()
{
// Call a method on the form.
_window.Close();
}
}
from code behind of Window:
webBrowser1.ObjectForScripting = new ScriptManager(this);
That worked, thanks!
I did the following:
[ComVisible(true)]
public class ScriptManager
{
protected Window Window { get; set; }
public ScriptManager(Window window)
{
this.Window = window;
}
public void CloseWindow()
{
this.Window.Close();
}
}
And in my Window (Loaded Event):
// Build browser
this.Browser = new WebBrowser();
this.Browser.Navigate(this.GetUri());
this.Browser.ObjectForScripting = new ScriptManager(this);
The client Javascript then does:
$scope.Close = function() {
window.external.CloseWindow();
}

JavaScriptInterface not added on API 18

I am using a WebView to execute some JS code in my app. This works fine so far on my KitKat (targetSdk) device, and also on a 2.2 (minSdk) emulator.
On a 4.3 Nexus 7 I encountered an error though. It seems as the JS object doesn't get injected.
Uncaught ReferenceError: android is not defined at null:1
Following a stripped down version of my code:
public void getData(Context ctx) {
WebView webView = new WebView(ctx);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new AndroidInterface(), "android");
webView.loadUrl("javascript:console.log(android.getInput());");
}
public class AndroidInterface {
#JavascriptInterface
public String getInput() {
return "FOO";
}
}
The JS itself gets executed fine, so doing console.log('FOO'); prints FOO just as expected. But as soon as I try to access the interface I get the ReferenceError. Any ideas?
Thanks in advance!
Could be a number of things. You can try the following to narrow it down:
insert a sleep (1sec is more than enough) between the add and the loadUrl. It's unlikely that the API is racey but it's easy to check.
load a page before calling loadUrl("javascript:..."). Call something like loadUrl("about:blank") or loadData("<p>foo</p>", ...); and sleep for a bit.
Calling loadUrl("javascript:...") internally evaluates the JavaScript in the current page, it doesn't perform a 'real' navigation. It might be that the interface that you're adding is not being picked up by the context in which your loadUrl("javascript:...") call is executed (especially that the 'starting' page the WebView loads after being constructed is a bit special) and that performing a navigation makes the WebView "pick up" the newly added interface.
Found a workaround, would still be interesting to know why it doesn't work on 4.3.
So instead of using loadUrl, I surrounded the JS by a piece of HTML and used loadData.
webView.loadData("<!DOCTYPE html><html><head><script type=\"text/javascript\">function go(){console.log(android.getInput());}</script></head><body onload=\"javascript:go()\"></body></html", "text/html", "UTF-8");

DOM is not being re-rendered after updating based on touch events (android, phone gap) [duplicate]

When ever I make a css class change, the changes don't always appear. This appears to happen when I have a touch event that adds something like a down class name to a button. The button doesn't update, and neither does anything else on the page. It's very inconstant in when it works. I've also noticed that sometimes my elements appear white with no content or anything. This is very frustrating!
Note:
There's a better solution as of Android 4.4+. It's a drop-in WebView replacement called CrossWalk. It uses the latest Chromium-kit and it's fantastic. You can read up about it here: crosswalk-project.org
Also, it appears that since Android 4.4, the invalidate() solution is no longer necessary and you can get away with using some of the other safer answer. I would only use this invalidate() approach as a last-ditch effort.
I'm answering my own question to hopefully help people out with the same issues as me.
I've tried several methods to making things better, even the all notorious -webkit-transform: translate3d(0,0,0); Even that didn't work all too well.
Let me share with you what did work.
First, use the most recent API. I'm using API 15. In your AndroidManifest.xml, make sure to enable hardware acceleration. If your version of API does not support this, then move on to the next bit.
If your version does support it, you can enable it by modifying your manifest:
<application
...
android:hardwareAccelerated="true">
Also, make sure that your manifest has the minimum supported API to the one that you are using. Since I'm using API 15, this is what my manifest has:
<uses-sdk
android:minSdkVersion="15"
android:targetSdkVersion="15" />
(Update: you should now modify that values in your build.gradle)
Now, in your primary CSS file for what will be presented in a WebView, add something like this:
body div {
-webkit-transform: translate3d(0,0,0);
}
Add onto the body div bit with any other element types you have; you can probably exclude images, ul, li, etc. The reason for applying this CSS style to everything is just by trial and error, and I found that brute-applying it to everything appears to work the best. With a larger DOM tree, you may need to be more-specific. I'm not sure what the specification would be, however.
When you instantiate your WebView, there are some settings you'll want to set:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.loadUrl("file:///android_asset/www/index.html");
appView.getSettings().setRenderPriority(RenderPriority.HIGH);
appView.getSettings()
.setPluginState(WebSettings.PluginState.ON_DEMAND);
}
Second to last, but crucial bit: I was reading through the source code for the WebView class and found this little tiny comment about force redrawing. There is a static final boolean, that when set to true will force the view to always redraw. I'm not huge on Java syntax, but I don't think you can directly change a static final attribute of a class. So what I ended up doing was I extended the class like so:
import org.apache.cordova.CordovaWebView;
import android.content.Context;
import android.graphics.Canvas;
public class MyWebView extends CordovaWebView {
public static final String TAG = "MyWebView";
public MyWebView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Warning! This will cause the WebView to continuously be redrawn
// and will drain the devices battery while the view is displayed!
invalidate();
}
}
Keep in mind, I'm using Cordova/PhoneGap, so I had to extend from the CordovaWebView. If you see in the onDraw method, there is a call to invalidate. This will cause the view to redraw constantly. I highly recommend adding in logic to only redraw when you need it, however.
There is one final step, if you are using Cordova. You have to tell PhoneGap to use your new WebView class instead of their own WebView class. In your MainActivity class, add this:
public void init(){
CordovaWebView webView = new MyWebView(MainActivity.this);
super.init(webView, new CordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}
That's it! Try and run your app and see if everything is much smoother. Before doing all of these changes, the pages would appear white, no CSS changes would be applied until after tapping on the screen again, animations were super choppy or not even noticeable. I should mention that the animations are still choppy, but far much less choppy than before.
If anyone has anything to add to this, just comment under. I'm always open for optimizations; and I'm fully aware there may be better ways to do what I have just recommended.
If my above solution did not work for you, could you describe your specific situation and what results you are seeing with Androids WebView?
Lastly, I have enabled this answer as a "community wiki", so anyone and everyone, feel free to make adjustments.
Thanks!
Edit:
With the most latest PhoneGap, you'll need to have your init() method look more like this:
public void init(){
CordovaWebView webView = new MyWebView(MainActivity.this);
super.init(webView, new IceCreamCordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}
I implemented kyle's solution and it solved the problem. Howewer I noticed a huge battery drain on android 4.0.4 when the app was open. Also after the change I had users complaining that the swiftKey keyboard was not working with my app anymore.
Every change in my app are triggered by a user action so I came up with a modified version that only trigger invalidate() after a touchEvent:
Handler handler = new Handler();
public boolean onTouchEvent (MotionEvent event){
super.onTouchEvent(event);
handler.postDelayed(triggerInvalidate, 60);
handler.postDelayed(triggerInvalidate, 300);
return true;
}
private Runnable triggerInvalidate=new Runnable(){
public void run(){
invalidate();
}
};
Never did any programming with Java so there might be a better solution to do this.
re: the redraw problem, you can force a redraw by reading a property from the element
so say you do this:
$('#myElement').addClass('foo'); // youre not seeing this take effect
if you do this afterwards:
$('#myElement').width();
it will force a redraw.
This way you can be selective instead of redrawing the whole page all the time, which is expensive
For me this issue was only happening on Samsung devices. I was able to fix it by disabling Hardware Acceleration for WebViews:
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Hope it helps.
As pointed out above and elsewhere - overriding View.onDraw() and calling View.invalidate will make for an unhappy battery / app performance will drop. You can also do a manual invalidate call ever x ms like so
/**
* Due to bug in 4.2.2 sometimes the webView will not draw its contents after the data has loaded.
* Triggers redraw. Does not work in webView's onPageFinished callback for some reason
*/
private void forceWebViewRedraw()
{
mWebView.post(new Runnable() {
#Override
public void run()
{
mWebView.invalidate();
if(!isFinishing())
mWebView.postDelayed(this, 1000);
}
});
}
I tried putting an invalidate call in WebViewClient.onPageLoaded() but this does not seem to work. While my solution could be better its simple and it works for me (im just showing a twitter login)
Please also see my answer at WebView fails to render until touched Android 4.2.2 , The idea is to export #Olivier's function to JavaScript so you can trigger invalidates from JS in sensible parts ... My god, yet another more hack !! :)
I had this problem because onPageStared and onPageFinished I show and hide a loading animation.
Since was injecting JS do my menus and stuff from the original website and noticed that injecting JS on onPageFinished force the webview to draw contents. Here is a example of what you can add at the end of onPageFinished
view.loadUrl("javascript:(function() { var select = document.getElementsByClassName('something')[0]\r\n" +
" if(select)" +
" select.style.display = 'none';})()");
Just clear the cache of webView in oncreate() and it works for me.
webView.clearCache(true);
You should enable hardware by modifying your manifest:
<application
...
android:hardwareAccelerated="true">
And in your activity onResume()
webView.postDelayed(() -> {
if (!isFinishing() && webView != null) {
webView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}, 1000);
And in your onPause()
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

Catch certain Javascript function calls in a WebView

I am writing a basic web browser that can only go to a certain website (developed and maintained by another company) for my work, however in order for the log in and the time spent on the site to be counted (two VERY important things for my company) you need to log out with a certain button on the site.
I looked at the page source and all that button does is call a javascript function (named something like doLogoff() or something similar) which on a normal browser simply closes the window that is created after you log in.
In my application everything is done in ONE window, there are no tabs (there are no need for them) and I'm not entirely sure what the call to close the window does to my application, but the site on the WebView simply stays on that page and only goes back to the login page if you click on a link.
Is there anyway to detect when a certain JavaScript function is called in a WebView? If I can bind that function and make sure the log out is actually being performed, then I can just make the webview go to the login page myself.
You can do that with a JavascriptInterface. The following example comes from the documentation. It works the other way round. You can create a function in javascript that will trigger java code in your activity.
You declare your interface in your java code.
public class JavaScriptInterface {
Context mContext;
/** Instantiate the interface and set the context */
JavaScriptInterface(Context c) {
mContext = c;
}
/** Show a toast from the web page */
#JavascriptInterface
public void showToast(String toast) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
}
}
You add the interface to your WebView
WebView webView = (WebView) findViewById(R.id.webview);
webView.addJavascriptInterface(new JavaScriptInterface(this), "Android");
And in your web page you can call the java method from a script
<script type="text/javascript">
function showAndroidToast(toast) {
Android.showToast(toast);
}
</script>
First, create the JavascriptInterface, prescribed by NathanZ.
Then override the function that you want to hook into, like this:
webview.loadUrl("javascript:" +
"var functionNameOriginal = functionName;" +
"functionName = function(args) {" +
"Android.showToast();" +
"functionNameOriginal(args);" +
"}");

Categories