What’s new for Cordova 2.7.0 Android

Another month rolls by, and as usual, another release of Cordova happens. Some of the big highlights this month are as follows:

  • Camera.getPicture() is fixed for Picasa images
  • Plugin.java is deprecated for CordovaPlugin, plugins must be updated to support this change. This has followed our deprecation policy.
  • device.name has been removed, since it’s useless on most Android devices, and doesn’t make sense on iOS
  • Plugins can now intercept URLs with question marks, number signs and escaped spaces.
  • URL parsing is now more robust
  • localStorage is once again persistant, this breaks WebSQL on Android 4.x

The last change is the most irritating and is one that we’re currently struggling with. Many people use WebSQL in their applications. The problem with this is that it’s not a supported W3C specification, and as such is something that not every platform supports. On Android 3.x and up, the Android team made it so that file URIs can’t open databases. A workaround from the early days of Cordova was used to get around this issue, but there are numerous design issues, and this isn’t guaranteed to work the same way as WebSQL. This is why, for now, we recommend that people use the WebStorage APIs instead of WebSQL, because it is supported across platforms, and is much less likely to break. And even when WebStorage does break, we will fix WebStorage, even if it means breaking WebSQL.

There is a proposed fix for WebSQL where Cordova applications use a different URI scheme, but that may not be implemented until after 3.0 is released. In the meantime, we recommend that other alternatives be used, but we recommend WebStorage. We apologize for the inconvenience since this is something that is beyond our control.

Froyo and the Deprecation Policy

I am pleased to announce that in Apache Cordova 3.0, we will no longer be supporting Android 2.2 Froyo. While this probably doesn’t actually affect to many people, this does mean that we are going to be able to get rid of our one dependency. We’re already getting rid of support for Android 2.1 and Honeycomb in Cordova 2.7.0 as well.

What does this mean?

This means that things might still work for you, but if something breaks, we will close your ticket as “Won’t fix”. Most Android tablets have upgraded to Ice Cream Sandwich or later, so unless you have that rare Google IO Samsung Galaxy Tab 10.1 that you didn’t bother rooting, you’re probably not running this version of Android. Now, since Honeycomb and later versions of Android share a lot of the same properties, chances are that things may still work, but I wouldn’t bet on it. As far as Android 2.2 and lower, we plan on actually removing the commons-codec library. Right now, because we’re an Apache project, we have to have a script fetch that Apache project’s binary separately so that our image handling would work. Removing the dependency means that it’s one less binary we have to fetch and one less moving part.

Why the change in the Deprecation Policy?

When we decided to drop support for Android 2.1 and 3.x, we did the six month wait. This basically meant maintaining code that nobody was going to use for another six months because of policy. We felt that this was too limiting, and decided to move to it being released based instead. We didn’t feel that it added to the stability of the project, but we wanted to avoid the breakage that happened when we introduced CordovaWebView.

When will you be dropping Gingerbread?

We drop Android versions once they get below 5% of the user base. Gingerbread is still at 45%, and there are still new low-end Android devices that are sold with Gingerbread, so we will be supporting that for quite a while yet.

What’s new for Cordova 2.6.0 on Android

This the first time that I’ve done the “What’s new with Cordova” blog post. This release was mostly a bug fixing release, but we did manage to resolve many issues that were long standing with the Camera. This is mostly based on the GitHub changelog:

  • CB-1700: EXIF information is now preserved on images taken from the Camera and the Gallery
  • CB-1796: captureImage now can save a copy of the photo to the gallery
  • CB-2305: InAppBrowser now has an API for executing scripts and inserting CSS
  • CB-1933: Changed button labels to an array for notifications!
  • New Native to JS bridge for Binary Strings!

Also, this is the last release that will be supporting Android 2.1 and the Honeycomb releases. Cordova may still work with these versions of Android, but we will be closing all issues related to Android 2.1 and Android 3.x as “Won’t Fix”. We have first announced this change six months ago, and this shouldn’t affect most users since is currently only 1.8% of all Android devices currently in the wild. We are currently discussing deprecating Froyo as well, since it is only 4% of all Android devices, but any future changes to support will be discussed on the Apache Cordova developer list, and will be subject to our deprecation policy.

What else is new in Cordova Android 2.4.0

Normally, Simon Macdonald, who also works on Apache Cordova, writes a blog post listing the new features that landed. However, there’s a feature that should be added to that post that I’ll explain below.

Additional Preferences in config.xml:

Certain features, such as the splashscreen, the error URL and timeouts in the past were undocumented, and would have to be added to the activity at the beginning of the class before we load the URL in the Java file, like this:


super.setIntegerProperty("splashscreen", R.drawable.splash);

Since the whole point of Cordova is so that you don’t have to write Java, and to promote web standards, we added these preferences to the config.xml, which should look like this:


<preference name="splashscreen" value="splash" />

The new project settings in config.xml can be found here.

Functional Testing of the Android Webview using Java: How purity was invented

A long while ago, we decided to start doing native testing of Android, mostly because we found that we were fixing the same things over and over again on Android in Java.  While the Javascript quality was higher, the Java code was generally the same as when I first wrote PhoneGap back in 2008.  So, we started with manual tests.  Then I got creative and threw in some JUnit tests back when I started working on CordovaWebView.

However, there was something that was nagging us about the manual tests.  Many of these tests required us to click on user interfaces that were implemented in HTML and Javascript.  First we tried to adapt things such as WebDriver to test this, but we found that this was extremely complex to get working on Cordova, and we only ever managed to get an abandoned branch to work with the custom CordovaWebView.  At the end this was completely unmanageable, and we abandoned this work.

However, the tickets remained, such as ones like “Automate JQTouch Tab test” or “Automate iFrame Test”.  The JQTouch one was abandoned because of these develpments, but along the way we discovered how to test the Android WebView using Java and JUnit.

The Android Test Framework gives you some very limited UI functionality tools.  These tools are designed for the Android UI and, they mostly consist of things like TouchUtils.  TouchUtils allows you to click in the middle of view like this:


touch.clickView(this, testView);

This may be super awesome for Android applications, but this sucks for things like Cordova, which is one giant view.  To prove this point, I ressurected some Canvas code that I wrote three years ago and I do some tests on it.

crap_setting

Above, you can see the click happen in the middle. It will always happen in the middle.  There is nothing that I can do to make it not happen in the middle.  This sucks.

I then googled around to see if I can get per-pixel support, and I found a blog post that showed this code:


long downTime = SystemClock.uptimeMillis();
// event time MUST be retrieved only by this way!
long eventTime = SystemClock.uptimeMillis();
Instrumentation inst = getInstrumentation();
MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, 300, 300, 0);
inst.sendPointerSync(event);

Now, this code is great, except that anyone who has done any Mobile Web Development knows that a pixel is not a pixel.  We have to factor in the screen density.  This is where purity comes in.

In purity, instead of giving the real pixels in Java, you pass in the WebKit pixels, and purity does the calculation required to do the touch where you want it.  The reason it’s called purity is because it’s simple, and it doesn’t clutter up your Javascript with event handlers and injection code.  Eventually, this may be adapted to be a plugin so that purity code can be written in Javascript, but entirely run in Java.

For example, in purity, you would call this to do a touch event:

touchTool = new Purity(testActivity, getInstrumentation());
touchTool.touch(50, 200);

OK, where is the code:

The code is in the Cordova Android test directory on my personal branch here, and it is under org.apache.cordova.test.util. It’d be awesome if someone wanted to make this a third-party plugin, especially those people who have extreme pain with UI interaction on Android.

Chrome Content View: Compilation, Comparison and Thoughts

One thing that we get asked a lot is whether Cordova should bring along its own WebKit when running on Android. The reason for this is pretty obvious to everyone at this point, especially those poor users stuck on Android 2.x. If you’re an Android 2.x user and you went to html5test.com, you would see something like this:

Yeah, that’s pretty bad, and it doesn’t get a lot better when we go to Android 4.1.

Of course, those are the WebViews that we have access to. If you compare them to Chrome:

Or you compare it with Silk on the Amazon Kindle Fire HD:

It just gets depressing. We know that it’s possible to have Webkit not suck on Android, but since the Android Team neglected the browser, we have to get desperate and do crazy things like build our own. The most reasonable candidate for us is to hack on Chromium, and the only thing that I can get building was the Chromium content view. So, I took my personal Ubuntu 12.10 machine, followed these instructions here, and after fixing a header I got this installed on my phone.

Now, that’s a good score. Sadly, this is a debug build so the performance on it is rather slow, but it does show what’s possible on mobile.  It’s actually comparable to what’s on a desktop version of Chrome minus WebGL according to this test.

However, the reality of the matter is something different.  Currently if you took the Chrome ContentView to html.adobe.com and tried to use any of the Adobe examples, the application would currently crash.

As far as us getting Cordova working on it, that’s going to take a lot of time, since the ContentView is structured very differently than the Android SDK’s WebView, and it’s not ready for primetime. Anything that we do will be a hack, and not suitable for the Android Market or anywhere else right now. Hopefully in a few month’s time, we can get something more solid and be able to have better WebViews on Android.

Advanced Tutorial: Using CordovaWebView on Android

Back on the lead-up to Cordova 2.0, we pushed for the implementation of Cordova WebView.  While this broke a lot of things, and irritated a lot of people, it seems that people are actually starting to use this new functionality.  Unfortunately, we also accidentally made a change to it in Cordova 2.2.0. In this post we step through an example that I wrote up using the existing example of the Action Bar from the Android SDK. It should be noted that this guide assumes that you are comfortable with working with Java.

You can find the example of how to use this on a GitHub repository: https://github.com/infil00p/CordovaActionView

Even though CordovaWebView does its best to try to encapsulate all the methods that are required for a working PhoneGap project, there’s still a few things that the activity that holds it has to maintain.  This is why we created the CordovaInterface. In this example, we take the default example of the ActionBar application, and we implement CordovaInterface on it.  When we start dissecting it, we see the following:

public class MainActivity extends ActionBarActivity implements CordovaInterface{
    private final ExecutorService threadPool = Executors.newCachedThreadPool();

This has to be here for the plugins to actually work. Following this, we have our typical variables both from the app, and what you’d normally find in a Cordova Activity:

    private boolean mAlternateTitle = false;
    private boolean bound;
    private boolean volumeupBound;
    private boolean volumedownBound;

    String TAG = "MainActivity-ActionBarTest";
    private CordovaPlugin activityResultCallback;
    private Object activityResultKeepRunning;
    private Object keepRunning;

    CordovaWebView mainView;

This is primarily to maintain the view, then in the create method, we basically do exactly what the old Android WebView example code does, and we find the view, and load our app:

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mainView =  (CordovaWebView) findViewById(R.id.mainView);
        mainView.loadUrl("file:///android_asset/www/index.html");
    }

Meanwhile, if you look at the XML file, you will see the FrameLayout for the main application. The ActionBar in this case is handled separately by the Android Application:


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <org.apache.cordova.CordovaWebView 
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" 
        android:id = "@+id/mainView"/>
</FrameLayout>

When using custom widgets, you have to include the full name of the object that you want to include. It should be noted that I made a mistake here and that “match_parent” should be used instead of “fill_parent”, since “fill_parent” will be deprecated. Back in the code, we add the following methods to implement the CordovaInterface.  The first that we add is getActivity.  This is expected to get the activity that actually contains the CordovaWebView so that we can handle intents that are passed to the application.  This is pretty self-explanatory:

    @Override
    public Activity getActivity() {
        return this;
    }

After this, we implement onMessage, which deals when a message is sent from a plugin to the activity itself.  This currently exists in DroidGap, and facilitates the app plugin functionality, such as the splashscreen, the spinner, and whether the application exists.  This is not entirely necessary to implement, so in this example we simply ignore everything but exit.

    /**
     * Called when a message is sent to plugin.
     *
     * @param id            The message id
     * @param data          The message data
     * @return              Object or null
     */
    public Object onMessage(String id, Object data) {
        LOG.d(TAG, "onMessage(" + id + "," + data + ")");
        if ("exit".equals(id)) {
            super.finish();
        }
        return null;
    }

Next is the most important, which is the setActivityResultCallback code and startActivityForResult. This facilitates the passing of information from activities such as the Gallery and the Camera back to Cordova. Without this the capture API will not work, nor will any plugin that relies on another activity to start. This does the majority of the Android housekeeping:

    @Override
    public void setActivityResultCallback(CordovaPlugin plugin) {
        this.activityResultCallback = plugin;        
    }
    /**
     * Launch an activity for which you would like a result when it finished. When this activity exits, 
     * your onActivityResult() method will be called.
     *
     * @param command           The command object
     * @param intent            The intent to start
     * @param requestCode       The request code that is passed to callback to identify the activity
     */
    public void startActivityForResult(CordovaPlugin command, Intent intent, int requestCode) {
        this.activityResultCallback = command;
        this.activityResultKeepRunning = this.keepRunning;

        // If multitasking turned on, then disable it for activities that return results
        if (command != null) {
            this.keepRunning = false;
        }

        // Start activity
        super.startActivityForResult(intent, requestCode);
    }

Next is cancelLoadUrl, which was once supposed to cancel the loading of a URL. We decided to get rid of this functionality, but this operation still exists on the CordovaInterface, and will hopefully be deprecated soon. However, we still have to have it.

    @Override
    public void cancelLoadUrl() {
        // This is a no-op.
    }

The last required method are the following two at the bottom. This is the thread pool and getContext(). Both of these are expected to exist for plugins to function, however one is deprecated, and this is getContext(). Initially the CordovaInterface was once the DroidGap activity, and since an Activity inherits the properties of the context, people would just use the activity, since we can’t assume that we’re using a DroidGap activity, we need to provide an Activity. We do this with getActivity, but some people still use the old getContext() API, which is why this is still here. Eventually we will be dropping this because having two methods that return the same thing is silly.

    @Override
    public ExecutorService getThreadPool() {
        return threadPool;
    }

    @Override
    @Deprecated
    public Context getContext() {
        return this;
    }

Android Lifecycle and Results:

Now, back to actually handling the result that comes back. This is where we use the stored callback and send the result to the proper plugin. This is pretty self-explanatory as well when you look at this code. The thing is that this isn’t a part of the CordovaInterface, but instead is a part of Android. This has to be implemented if you want your Camera, Gallery and other Activity-based plugins to work.

    @Override
    /**
     * Called when an activity you launched exits, giving you the requestCode you started it with,
     * the resultCode it returned, and any additional data from it.
     *
     * @param requestCode       The request code originally supplied to startActivityForResult(),
     *                          allowing you to identify who this result came from.
     * @param resultCode        The integer result code returned by the child activity through its setResult().
     * @param data              An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
     */
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);
        CordovaPlugin callback = this.activityResultCallback;
        if (callback != null) {
            callback.onActivityResult(requestCode, resultCode, intent);
        }
    }

The other big thing is pause and resume. One of the things that we have to keep in mind is what happens to Cordova and the plugins when they pause and resume. Many of them will save their current state, and these events need to be handled. This is the most barebones code to deal with pause and resume. This does not pause any WebKit timers, so your JS will still run in the background thread, but this will handle Java states.

    @Override
    /**
     * Called when the system is about to start resuming a previous activity.
     */
    protected void onPause() {
        super.onPause();

         // Send pause event to JavaScript
        this.mainView.loadUrl("javascript:try{cordova.fireDocumentEvent('pause');}catch(e){console.log('exception firing pause event from native');};");

        // Forward to plugins
        if (this.mainView.pluginManager != null) {
            this.mainView.pluginManager.onPause(true);
        }
    }

    @Override
    /**
     * Called when the activity will start interacting with the user.
     */
    protected void onResume() {
        super.onResume();

        if (this.mainView == null) {
            return;
        }

        // Send resume event to JavaScript
        this.mainView.loadUrl("javascript:try{cordova.fireDocumentEvent('resume');}catch(e){console.log('exception firing resume event from native');};");

        // Forward to plugins
        if (this.mainView.pluginManager != null) {
            this.mainView.pluginManager.onResume(true);
        }

    }

    @Override
    /**
     * The final call you receive before your activity is destroyed.
     */
    public void onDestroy() {
        LOG.d(TAG, "onDestroy()");
        super.onDestroy();

        if (this.mainView != null) {

            // Send destroy event to JavaScript
            this.mainView.loadUrl("javascript:try{cordova.require('cordova/channel').onDestroy.fire();}catch(e){console.log('exception firing destroy event from native');};");

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

Finally, it’s important to handle any new event that appears to the application, which is what we do here with onNewIntent:

    @Override
    /**
     * Called when the activity receives a new intent
     **/
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        //Forward to plugins
        if ((this.mainView != null) &amp;&amp; (this.mainView.pluginManager != null)) {
            this.mainView.pluginManager.onNewIntent(intent);
        }
    }

The final result of all this is the following UI that works on Gingerbread, Honeycomb, ICS and Jellybean.

At this point, you could use fragments to have multiple CordovaWebViews, or add plugin code to manipulate the CordovaWebView so that it runs Javascript through a plugin. The reason you would want to do this is for further integration with the platform. This is clearly not for the average Cordova user, but instead for people who are comfortable with Java but still want the APIs that Cordova offers.