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.

11 thoughts on “Advanced Tutorial: Using CordovaWebView on Android

  1. Pingback: entry.title | SDK News

  2. Pingback: Tutorial: Embed PhoneGap as a Subview in your Native iOS Application | Devgirls Weblog

  3. Hi,

    Thanks for sharing this !
    I would like to ask you if you have any project as example using CordovaWebView with Fragments?

    I’m trying to insert it in the fragment
    public class TestWebView extends FragmentActivity implements CordovaInterface{}
    but I can not extend to Activity, must to be Fragment. So is there any way to convert it to fragment?

    Thanks in advance!

  4. Pingback: PhoneGap Community Roundup – December 2012 | SDK News

  5. Hi,
    Thanks for the explanation.
    Its working well for me when using a web url, like “www.google.com”.
    but when trying to open from the apk assets files via cwv.loadUrl(“file:///android_asset/www/index.html”);
    it fails with the following logcat error:
    CordovaWebViewClient.onReceivedError: Error code=-1 Description=A network error occurred. URL=file:///android_asset/www/index.html

    any idea?

    Thanks,
    Ofer

  6. Many thanks, this tutorial is fundamental to use CordovaWebView in Android.

    One small question: the variable activityResultKeepRunning is not used. Is it correct?

    • Hi, its too useful to me..thanks
      here u r using cordova 1.9 version. In this we don’t have readAsArrayBuffer implementation. I found that in cordova 2.4 and above version. I try to upgrade to 2.4. but throws lot of IPlugin Errors. can u help me to fix this?

  7. Hi, its too useful to me..thanks
    here u r using cordova 1.9 version. In this we don’t have readAsArrayBuffer implementation. I found that in cordova 2.4 and above version. I try to upgrade to 2.4. but throws lot of IPlugin Errors. can u help me to fix this?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>