Phonegap / Cordova + AppCache

Cordova is great, right? Develop once, run everywhere etc etc. But what about updates? What about being backward compatible with all the new web services you are developing on the site, packaging the app and worrying about versions… no, we web developers can’t stand it.

First order of business – how to do updates. Common misconception is that traditional binary app stores (ios/android) don’t want the apps to self-update. It’s actually not true – you are allowed to self-update as long as the code you are updating runs in WebView. Bingo!

There are solutions on the market that give you such self-update mechanism, for example WorkLight that was recently acquired by IBM. It’s really not terribly hard to write this yourself if all you need is updates – get a zip file with all html/css/whatnot, download when application starts, extract, then actually render the UI.

However, there exists a system that is designed to do just that, and it’s arguably pretty good at that. The support for AppCache is very good both on modern desktop and mobile. The only question is how you’d use AppCache with Cordova.

Well, obviously your app can be a web site. Or web site can be the app… It’s really the same for web developers. I call most of the sites I create applications, because they are applications, from the customer point of view. Plus they can be pinned / clipped / made into an icon

I Steps on your site
1) Make sure you have offline manifest on the page that will be opened by Cordova
2) Double check – is there a redirect from the URL you think you are opening and actual URL that opens up? Appcache doesn’t work with redirects, so you have to have correct URL. For example, i had http://localhost/offline redirect to http://localhost/offline/ – and it didn’t work.
IIa Cordova iOS steps:
2) Allow Cordova to go out on the internet. In your project, go to Resources/Cordova.plist . Add record to External Hosts with your server name. You can just put star (*), but if someone makes your side redirect elsewhere you are in trouble – so only use for development.
3) Make sure cordova opens links inside its own webview, instead of safari, by changing OpenAllWhitelistURLsInWebView property to Yes.
IIb Cordova Android steps:
2) Allow Cordova to go out on the internet. In your project, open res/xml/config.xml and uncomment  <access origin=”.*”/> (this is only for development, see note in step 2 for iOS)
3) Add <preference name=”stay-in-webview” value=”true” /> to stay inside cordova
4) Adjust Android Cordova to enable AppCache. For some strange reason, it is not enabled by default for Android (while local storage and SQLLite are) – see setup() method in CordovaWebView.java file. It looks like in iOS UIWebView has it on by default and doesn’t let you mess with it. To fix this, just go into the only java file you should have in your app, under src/your.name.space/CordovaActivity.java (your file name could be different) and adjust the onCreate method to be like so:
public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.init(); // Initialize appView, since it's only initialized inside loadUrl by default
        android.webkit.WebSettings settings = super.appView.getSettings(); // get WebView settings
        String appCachePath = this.getCacheDir().getAbsolutePath(); // Set path to appcache
        settings.setAppCachePath(appCachePath);
        settings.setAllowFileAccess(true); // Let browser write files - doesn't work without this
        settings.setAppCacheEnabled(true); // Enable app cache
        super.loadUrl("file:///android_asset/www/index.html");
}
Update June 4 2013: Per comments, this is fixed in Phonegap 2.5, so code above in #4 is not needed anymore (i.e. leave it alone the way cordova generates it, don’t modify)
III Finally, the Cordova app
This one is very simple, as you might imagine. In www/index.html all you need to have is:
<!DOCTYPE html>
<script type="text/javascript" charset="utf-8" src="cordova-2.0.0.js"></script>
<script>
    function onBodyLoad()
    {        
        document.addEventListener("deviceready", function(){
            location.href = "http://192.168.2.108/offline/";
        }, false);
    }
    </script>
<body onload="onBodyLoad()"></body>
Replace the URL with your local testing site that you can start/stop to test how offline is working, or with any other URL.
That’s it, start the app!

Actually using Cordova APIs

One caveat is that at least on Apple AppStore the wrapped  site has to be more than site. It has to have some additional functionality that is not available via the web. And that’s where Cordova APIs come to the rescue, with access to all the interesting APIs a regular WebView/UIWebView doesn’t have access.

The first thing you have to do is detect that web site was opened from inside Cordova. It doesn’t look like there’s currently an API in cordova.js to tell us it loaded inside the Cordova web view. The only semi-working solution is to detect that onDeviceReady didn’t fire – but you have to wait to determine this fact, and that will delay your page loading in regular browser. Easier way is to pass some querystring parameter, url?fromcordova=true , and then include javascript and execute ondeviceready handler based on this parameter.

Second, cordova javascript actually has two versions – one for iOS and one for Android (and I am assuming others for other platforms). File name is same, content is different. The reason is that on iOS it’s using custom URL handler gap:// to communicate with Objective-C side. On Android, it’s overriding exec, prompt and alert methods in JavaScript and then on Java side in DroidGap class it’s intercepting these calls and actually using them to interact with JavaScript side (especially prompt, that can return data back to JavaScript). So the platform ID has to be another parameter in your URL, for example: url?fromcordova=true&platform=iOS

 Note: the above setup instructions are for Cordova 2.0.0 – in previous versions some things still had PhoneGap names.

28 Comments

  1. joe says:

    Artem, this worked great for android. thanks!
    One question: if I make a change on the website, how can I get the app to load the new file(s)?

    • Artem says:

      Browsers provide javascript API to communicate with manifest functionality, so you can subscribe to going online/offline or receiving updates. You can also check for updates forcefully (for example on timer). For example see here http://appcachefacts.info/ :

      if (window.applicationCache) {
      applicationCache.addEventListener('updateready', function() {
      if (confirm('An update is available. Reload now?')) {
      window.location.reload();
      }
      });
      }

  2. Zhenya says:

    Have you been able to use cordova’s exec() from the cached JS that arrived from the web into the appcache? My experience is that under this setup on iOS, cordova stops pumping the command queue, and therefore any calls to exec() simply queue the command invocation request and never make it to the native side. My hypothesis so far is that gap:// navigation requests are somehow mis-treated by the WebView’s ‘offline’ mode navigation scheme. Have you been able to use exec() successfully while running from a cached app?

    Thank you!
    Zhenya

  3. Hey Artem,

    Thanks for the information in this post. I wish you would have created an issue on JIRA (or a pull request) and we could have gotten this into a build almost a year ago.

    Simon

    • Artem says:

      I am assuming you are referring to appcache support in android . Well, I didn’t think that was a bug – so I didn’t report it anywhere.
      Is it changed now, should I still report it? I actually patched the webview class first, then found a way to do it during startup without having to rebuild cordova jar.

    • Artem says:

      oh yeah, i see it in the 2.5 release notes

  4. Harris says:

    I use the same technique. If I open my app with WiFi on it loads the page. If then I start the app with wifi closed, it produces application error that the url doesn’t exist.
    Isn’t this what AppCache is all about? If you cannot find the url load the cached version.

    Does anybody has an idea on what’s wrong?

    Cordova 2.8.1

  5. Anneleen says:

    So, I tried using cache as explained for 2.5. Turns out it doesn’t work without step 4… Very strange!

  6. Anneleen says:

    I do have one question though, could you elaborate a bit more on the actually using cordova API’s? You see, I was so happy I got the caching to work in Phonegap, but just now realise that it’s no use, cause I have to include my manifest file in my app locally, which still means I have to update the APK and upload it on the AppStore. Is there a better way for doing this?

  7. Ming says:

    I want to create an iOS phonegap app using the instruction here though I heard phonegap app are more likely to be rejected by apple because it is not native enough.

    Anyone have a iOS app like this got approved by apple?

  8. McKamey says:

    Excellent article. BTW, `cordova.js` doesn’t even need to load on the bootstrap page to make this all work. In fact you can forgo the JavaScript entirely:

    • Artem says:

      Yes, if you don’t want access to phonegap APIs this is feasible inside a regular webview / uiwebview. The deviceready here is an example of such feature – native app communicates to javascript that it’s ready to accept communication.

  9. Kompella says:

    Hi Artem, Thanks for this information. I have few HTML5 pages wrapped around using a phonegap/cordova index.html which is more like a menu page. Each of the HTML5 page is a clickable link from the menu page. While each of the HTML5 pages itself is offline cache enabled, but when I go out of one of the HTML5 page back to menu page and then click on another HTML5 page, then it does not open up in offline mode ( although all the HTML5 pages are cached when connected online). Could you please help as to what I should do in the menu page-index.html to rectify this?

    • Artem says:

      So this is slightly complicated – the offline cache file can contain references to all the .html pages on your site – but depending on the site, it may not be advisable to actually do that. The example that is normally given is wikipedia – you wouldn’t want to have all wikipedia files in the manifest. So multiple manifests must be created (per page) – but that means that the user has to visit these pages for them to get cached.
      Also, if the child page contains references they are not downloaded – only the html of the page is downloaded.

      Your scenario should work though, so you must check for some gotchas listed at http://appcachefacts.info/ . Specifically in your case it can be one of the following:
      – If any of the files mentioned in the CACHE section can’t be retrieved, the entire cache will be disregarded.
      – Over SSL, all resources in the manifest must respect the same-origin policy

      You can check the behavior configuration using chrome via chrome://appcache-internals/ URL to troubleshoot

  10. Tarhe Oweh says:

    Good day Artem. I just went through the write up and it was good. I am still learning how to build Apps with PhoneGap and I decided to try out Caching/Offline Mechanism today. After reading through some tutorials I found online and yours, I was able to put down some codes. I am using PhoneGap 2.9.1 and jquery Mobile 1.4

    I decided to try out different ways to achieve what you described. I uploaded all my test files to Wamp Server and it was fetched successfully but with a little delay in the process. I checked if Cordova will fire, it fired well (mind you, I uploaded cordova.js, sqliteplugin.js and others to the server too) and the page that is being fetched is profile.php (with a .php extention which did not pose problem at all). All dynamic images, text were cached without me even listing them on the manifest file and this made me to suspect foul play. I rebooted the phone that I was using to test the Apps and I tested again to make sure the page was really cached, and it was cached. I tried again in Airplane mode and it was the same result.

    My problem now is that, the phone loads up the cache page data only even when network is available and this prevent changes in the database to show on the page, any solution please?

    • Artem says:

      So this is all pure manifest / appcache behavior, not really related to cordova. What you are describing sounds like a problem with dynamic page being cached.

      On the web, the file extension in the URL makes no difference – it’s the content that matters. And sounds like you tried to cache a dynamic page – you don’t really do this with dynamic pages.

      Refer to http://appcachefacts.info/ for quick rundown on how appcache operates.
      So specifically you are hitting two scenarios with appcache:
      1) Images are cached – the browser actually cached them without manifest, that’s what browsers do
      2) Dynamic page is cached – since you included manifest on page, it will cache it even if it’s dynamic on server. You shouldn’t really put manifest on dynamic page – the only way this will reload is if the manifest file changes on server – so you’ll have to be updating manifest all the time. That defeats the purpose of manifest.

      The only way to do this is just to give static html to client, and retrieve dynamic parts of the page using ajax. This is actually the target for the manifest spec – for SPA applications, you have very static html, and get all the data using ajax.

  11. David says:

    Hello,

    I’ve made an application which uses appcache to work offline. When the updateready event fires I giva a message and reload the page using window.location.reload(). The problem is that after this call deviceready doesn’t get called again and the cordova plugins stopped working. How should I reinitialize the app after updating the appcache without losing the cordova functionalities?

    I’m using cordova 3.5 and using android to test.

    • Artem says:

      Searching for this, looks like several versions of cordova had this bug – not 3.5 specifically, but it did show up before. It may well be a bug again in 3.5
      Try 3.4 or try reloading several times if deviceready fails to trigger.

  12. Benedikt says:

    Hi, I realise this post is a bit old, but I thought I’d give it a shot:
    How did you deal with the case of the user going offline after downloading the app and starting the app offline? To my mind the user would need to be online again in order to download the appcache + files for the first time? Which is a bit tricky to understand for users who are used to downloading an app and having the content available/included. Or was this not an issue in your case?
    Thanks!

    • Artem says:

      Good question – this is something that is not really possible to address with this approach. What we ended up doing is a partial downloaded experience – i.e. you can get some functionality bundled using regular cordova process (i.e. it’s compiled into the app), but the rest is on the web.
      This works well for apps that require sign-in or do some sort of search online.

      It is theoretically possible to do though – the appcache and cached data is stored in the app sandbox folder (i only checked on ios – android should be similar) – so if you reverse engineer that you can pre-populate at least initial version of content. I wouldn’t recommend though – as it relies on inside knowledge of how browser works on specific version of a platform.

  13. Marshall says:

    I’m currently using this app cache technique for my app. All the HTML/CSS/JS is hosted on a server (which is only accessed by the app) and all works well since I can simply update the remote code and app cache will auto-update the local app’s version. But I just read on the Cordova best practices section that “Invoking Cordova JavaScript functions from a remotely-loaded HTML page (an HTML page not stored locally on the device) is an unsupported configuration. This is because Cordova was not designed for this…”. They mention some issues like same-origin challenges, and indeed I’m having some issues with OAuth 2 that I believe are related.

    Is Cordova advising against doing this and do you know of any problems with this approach?

    • Artem says:

      Yeah, that’s new – I did find this here https://cordova.apache.org/docs/en/edge/guide_next_index.md.html#Special%20Considerations_loading_remote_content


      Invoking Cordova JavaScript functions from a remotely-loaded HTML page (an HTML page not stored locally on the device) is an unsupported configuration. This is because Cordova was not designed for this, and the Apache Cordova community does no testing of this configuration. While it can work in some circumstances, it is not recommended nor supported. There are challenges with the same origin policy, keeping the JavaScript and native portions of Cordova synchronized at the same version (since they are coupled via private APIs which may change), the trustworthiness of remote content calling native local functions, and potential app store rejection.

      Let’s address the points:
      1) No testing – true, hence this blog article. You can test yourself, and since you are not upgrading cordova all the time, i.e. the app is relatively static, you can actually test this fully.
      2) Same origin policy – the challenges that I’ve seen are actual CORS support/bugs in mobile browsers. Generally this works just fine with OAuth, there’s nothing special since it’s just a web page . Test your app using your actual site name (outside of cordova), and try to emulate mobile as much as you can. You can point a mobile browser (from device) at your dev box for a pretty good test. We also used proxy (e.g. fiddler) to emulate actual production behavior on mobile device (set up proxy on the device to point at dev box, and change host file on dev box point production domain at it)
      3) Keeping javascript and native cordova synchronized – I am addressing this somewhat above, where I talk about the cordova.js file that you’ll have to serve. Again, since you are staying on same version, it’s not a problem.
      4) Trustworthiness – agreed, and I do address this. You have to be careful with your whitelist, and obvisouly careful about keeping it https. Arguably, someone hacking your site is even worse then hacking your app.
      5) App store rejection – wow, it feels like this is written based on my post and SO question, thanks :) That’s my main problem with this approach, frankly. For a AAA app that we were doing, this was a big question. We had to introduce enough app-only features to make it different from the web site.

      I may have to update the article somewhat with these points, however main advantage still remains – just look at this https://play.google.com/about/developer-content-policy.html
      An app downloaded from Google Play may not modify, replace or update its own APK binary code using any method other than Google Play’s update mechanism.
      meh…

Leave a Reply