Thursday 9 February 2012

Sencha Touch 2 + PhoneGap = Blank Web Browser Window Opening On An Android 3.2 Tablet

I was trying to make a Sencha Touch 2 app with some CSS3 animations in it performing reasonably on the Galaxy Tab with Android 3.2. When running the app in the Android web browser there were two major problems: the view was flashing after every transition quite unpleasantly and some bars of the chart were appearing/disappearing randomly, there seemed to be some problem with the page refresh.

I tried creating a simple native Android app and running the Sencha Touch app in a naked WebView. That didn’t work at all, the native WebView is more or less useless without an additional tweaking.

Finally, since I wanted to have an APK as an end result anyway, I’ve wrapped the app in PhoneGap 1.3. Immediately, it started behaving much-much better, the old problems were gone and the performance of the animation was close to native, and close to that on the iPad.

All this at the cost of a new major problem though: the very first interaction with the app after it had started was always causing a blank web browser window to appear, with about:blank URL in it. Since this window covered the app, the impression was like the app suddenly disappeared, like if it had crashed. The app was running just fine though, and after retrieving it from underneath the web browser one could appreciate all the benefits of PhoneGap wrapping.

While I was investigating this problem, PhoneGap 1.4.1 was released, but it didn’t fix the ‘about:blank’ problem.

I was able to find the reason for the about:blank window in the source code of PhoneGap. It is a minor bug that wouldn’t matter in most normal circumstances. There is a bigger problem that causes the minor bug to manifest, and I still don’t know whether that mysterious bigger problem caused by Sencha Touch or by Phone Gap. However, I fixed the minor problem, created a custom PhoneGap build, and everything works just fine now.

Below, I will describe the details of my investigation. I will also submit this discourse to Phone Gap in a hope that they will fix the bug in one of the coming releases.

I was debugging on a device, reproducing the problem and checking what LogCat says at that time. The message that corresponded to the blank browser window appearance was quite obvious:

I/ActivityManager(80): Starting: Intent { act=android.intent.action.VIEW dat=about:blank cmp=com.android.browser/.BrowserActivity } from pid 404

This means that some process has sent to Android an Intent requesting to view a specific URL, and the URL was specified as “about:blank”. The natural Android’s reaction to such a request is to start its web browser and to display in it a blank page. The next question was: which exactly code sends such an Intent? I obtained the source code of Phone Gap and searched through it. There is only one place in the code where such an Intent is created, in DroidGap.java:

public void onDestroy() {
    ...

        // Load blank page so that JavaScript onunload is called
        this.appView.loadUrl("about:blank");

    ...
}

A question that could be asked here is why the onDestroy() method of the DroidGap activity is invoked at the first interaction with the app, and this is exactly where the mysterious bigger problem manifests itself. Since the app continues to run successfully, I guess that another instance of DroidGap is being quietly created and destroyed behind the scenes. It might have been totally unnoticed if not a minor bug that I had discovered, so let me concentrate on the bug for now and report the other findings later.

The problem with PhoneGap is that the above line of code doesn’t actually achieve what it is expected to achieve according to the comment, i.e. it doesn’t load a blank page into this.appView, where this.appView is a reference to the instance of the WebView that is used by PhoneGap to display the app.

This is because the appView has a WebViewClient attached to it (GapViewClient or CordovaWebViewClient depending on the version of the source code). That client has the shouldOverrideUrlLoading() overriden, which means that each time the WebView is about to load a URL, that method is invoked first. If the method returns false, the WebView proceeds with loading whatever URL was given to it, otherwise it does nothing and it is up to the developer to do any appropriate coding to react to the URL.

If we look inside the shouldOverrideUrlLoading() method, we’ll see that it checks for some special cases, like URLs beginning with ‘mailto:’, ‘sms:’ and so on, but ‘about:’ isn’t one of those special cases. For those URLs that aren’t special, the WebViewClient does the following:

// All else
else {

    ...
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setData(Uri.parse(url));
            ctx.startActivity(intent);
    ...
}
return true;

Basically, it creates an Intent, attaches to it the URL (which the WebView was intending to load) and throws the Intent to the Android OS to handle (and so open a blank browser window). Then it returns true which tells the WebView something like “don’t bother handling this URL, I’ve already done everything that was needed”.

So as a result of this line of code:

this.appView.loadUrl("about:blank");

WebView does nothing while WebViewClient passes “about:blank” to Android to handle. Thus the developer’s intent to “load blank page so that JavaScript onunload is called” is circumvented, and this is a bug.

To fix the bug, I’ve added to shouldOverrideUrlLoading() the following one-liner:

else if (url.startsWith("about:"))
{
    return false;
}

So that for a URL like “about:blank” WebViewClient tells WebView: go on and load this URL, I don’t care.

I’ve created a custom PhoneGap JAR with this patch and it works fine, the dreaded blank page doesn’t appear anymore.

Now a few thoughts on what’s going on behind the scenes, why a DroidGap Activity is being destroyed invisibly. Here is the complete logging that accompanies the appearance of the problem:

W/webview(404): Stale touch event ACTION_DOWN received from webcore; ignoring
D/DroidGap(404): DroidGap.startActivityForResult(intent,-1)
I/ActivityManager(80): Starting: Intent { act=android.intent.action.VIEW dat=about:blank cmp=com.android.browser/.BrowserActivity } from pid 404
W/WindowManager(80): Failure taking screenshot for (230x135) to layer 21010

When I tried to find out which code could be calling startActivityForResult() on DroidGap, I’ve found myself digging into the Capture class and methods like captureImage(). So it looks like some failed attempt of capturing a screenshot. But why would it happen when the only thing I was trying to do is to press a button (any button) on the app right after its startup?

If anybody knows an answer to this question, please let me know!