Cordova-app-loader

When developing, I didn’t see any issues (and still don’t). I then released my app to both app stores. Deleted my dev versions of the apps and installed the ones from the app stores (on Android 4.4.4 with Crosswalk and iOS 8.1).

Problem 1: Pushed new code to the server and neither of the apps on my test devices detected the new manifest. Could not figure out why. Reinstalled dev versions and it all worked fine. Suspected it might have something to do with the app store deployment being different, but generated and manually installed release apk and ad-hoc ipa onto my devices and they worked fine. Also one of my users was prompted to do the update. Never did figure out why this didn’t work.

Problem 2: I had added a new template file and updated my main js and css files in the update. When the update was applied on an Android phone, the new template file and js seemed fine. The new CSS did not get applied, so things looked bad. I didn’t see this problem on iOS - the updates were applied properly.

There is always the possibility that some or all of this was due to a server problem at the time and that the appropriate data wasn’t being sent down to the app. I’m about to submit new apps to the app stores, and will leave cordova-app-loader in for now and give it another go. I’ll let you know how it goes.

If I can’t get it working, it appears that application cache for “hot code push” is a viable way to go. Here’s what I’ve learned:

There’s a nice description of how @mikemintz implemented it here:

and described by @SidneyS here:

More details on application cache:

Notes:

  • uses built in browser functionality - good
  • You need to put all the cordova and cordova plugin files on your server as well as your app files, and have different versions per platform. Something like this from @andrewreedy could help: https://github.com/andrewreedy/cordova-loader
  • it appears that you don’t need to package any of your app code into your binary (which makes the binary lighter), but on first run (and until all the files are cached), you will be running your app from the server, and all the files will be downloaded and cached in the background
  • if any of the files listed in CACHE can’t be retrieved, the entire cache is disregarded
  • I had thought that it re-downloaded everything in the manifest, but according to the second link above - “When updating an existing appcache, the browser sends standard If-Modified-Since headers, so it skips re-downloading files that have not changed.” which is nice, although in practice you’re probably uploading newly generated versions of the files, even if they haven’t changed.

Hi everybody

i found some time to test the issues mentioned here.

My results:

HTML files

If a new HTML file is added to the $stateProvider its not loaded right

.state('app.search', {
    url: "/search",
    views: {
     'menuContent': {
     templateUrl: "templates/searchnew.html" // was search.html before 
    }
   }
  })

This issue seems to be caused by the ionic view cache, the request to the new file points to

GET file:///android_asset/www/templates/searchnew.html net::ERR_FILE_NOT_FOUND

Hash issues

Sometimes it happens to me, that a changed file created the same hash value. I m using the grunt task to create the manifest files ( incl. hash )
Not sure if this is just a local issue or not. But it only happens in production mode ( files get minified and uglyfied )

Bootstrap

As @rajatrocks mentioned here

is really important to wait for all scripts to load,

Manifest

The request to get the latest manifest from the server is called without a cachebuster like manifest.json?randomNumbersHere

It happens to me that the server delivered a cached version instead of the current version

Regards

I think that’s the same issue, where the html files are not ending up in the right place, but sitting in the place they downloaded which i guess is not available to the app

I have this component working great in our app right now, fonts and everything, autoupdating, etc., … all except the template files.

I tried clearing the webview cache also, which is good to do anyway, but it didn’t help for this problem

Other than the html templates, everything working fine…

I’m going to get off to some other things for now, but I’ll circle back to this to see if somebody else can help or if not, to dig in a little deeper

Great info, thanks!

Do you have the problem with the HTML file even if you terminate the app and restart it?

The easiest fix for the manifest being cached seems to be to add the cachebuster manually when specifying the CordovaAppLoader options

            var loader = new CordovaAppLoader({
                fs: fs,
                localRoot: 'app',
                serverRoot: $rootScope.vars.baseURL + 'server_cache/',
                mode: 'mirror',
                cacheBuster: true,
                checkTimeout: 10000,
                manifest: 'manifest.json' + "?" + Date.now()
            });

that’s a good idea for the manifest.json to force reload, was too sticky

i think this template problem is that the updating files load into some other directory cdvfile://localhost/persistent/ which loads fine for the js and css which get loaded explicitly in the generated document, but not the templates which are still using the default place for templateUrl for the angular application

any amount of restarting, rebooting device, etc. doesn’t seem to fix this

worse comes to worse… might have to do this for templates: https://www.npmjs.com/package/gulp-angular-templatecache

kind of a drastic solution, but should work

I just updated my app on the play store and noticed this problem as well. I took my wife’s phone, which had the last production release of my app, and updated it from the play store. I had changed one of the templates. and after the update that page no longer worked properly. Going into Settings > Apps > My App and clearing the application’s cache fixed the problem.

Putting all the templates into index.html might fix the problem? Let me know if you figure it out.

I think I’m giving up for now, removing CAL for the time being and resubmitting.

If I can’t apply a new update from the app store and know the app will work (regardless of what’s happened before on the device), then I think that’s a problem…

Hi everybody

i ve a fix for the html issue.

I added an HTTP Interceptor to my app.js in the config part. Since every template is fetched via HTTP its easy to change the path to the stored file path

This is just a quick hack but works well on my first tests

$httpProvider.interceptors.push(function () {
                
                var calFiles =  [];  // List of cordova app loader files 
                if(!!localStorage.getItem('manifest')) {

                    //we have a manifest, lets parse it 
                    var manifest = JSON.parse(localStorage.getItem('manifest'));

                    // show me the content ( just for debugging )
                    console.log(manifest);

                   //Iterate over all keys in files 
                    for (var key in manifest.files) {
                        //add them to our cal array
                        calFiles.push(manifest.files[key]);
                    }
                }
                
                // debug can be removed
                console.log(calFiles);
                
                
               //the interceptor code
                return {

                     //Intercepting a request 
                    'request': function (config) {
                        
                        console.log(config.url);

                        // check if the config.url of the request matches a file in manifest and its html
                        calFiles.forEach(function (item) {
                            if(item.filename === config.url && config.url.substr(-5) === '.html') {
                                console.log(item.filename , 'is in the manifest changing ');
                                
                                //change the url to the stored item 
                                config.url = 'cdvfile://localhost/persistent/app/'+item.filename;
                            }
                        });
                        // Return our modified request
                        return config;
                    },

                    //response without modification
                    'response': function (response) {
                        // same as above
                        return response;
                    }
                };
            });

Regards

1 Like

It would be great if anyone could test my workaround just tested it on a really simple APP ( Starter ) . i now the code is not very robust ( missing checking files are available in the filesystem , request already points to the right location … )

Also i m mit sure if its possible to integrate this into the loader directly, because the workaround works only for angular / ionic projects

Hi everybody

since the angularJS interceptor works great for, i checked several issues again and how they can be solves

Works out of the box:

  • CSS
  • JS (if you take care of the order how the scripts get loaded
    (load array of manifest.json).
  • Fonts

Works after the great work of @rajatrocks

  • Bootstrap of the application after all scripts are ready ( please PR your solution )
  • Generate the a manifest.json even no - is in the file path with grunt

BTW where are the PRs for your solutions :wink:

Works in AngularJS / Ionic apps

  • HTTP templates even with ionic template cache
  • Images ( extends the interceptor or write a directive / watcher like in ngImageCache

So from an Ionic/AngularJs dev view everything seems to work, but there is a problem which can not be fixed without a native plugin ( hope it can but i didnt found a solution ):

  • Interception of file:// calls
    Calls to assets in cordova are done via ( file:// urls) which can not be intercepted with vanilla js ( for http there is a simple solution Hijack AJAX Requests Like A Terrorist

So from my point of view cordova app loader works fine. And its well integrated into my development workflow. Also its nice to have the app and the backend always in sync ( update of the backend causes auto. an update of the app )

I m also working on a simple server app for managing different users and versions of an app ( i think the ionic team is doing the same, but its not ready yet). To increase the speed of development and testing

Regards

That’s great that you’ve figured it all out! After thinking about it, I’ve decided that CAL isn’t a good solution for me for the reason I mentioned above - if I can’t guarantee that an app store update will always work, then I think the solution is too risky. And with CAL, that’s exactly the case - files that have been cached locally, potentially improperly, will continue to be loaded even after an app store update AND a server update. I believe this is what happened in my case:

  1. User installed v1 via app store.
  2. User updated to v2 via CAL. Files now being loaded from local cache. Manifest saved in local storage.
  3. User updated to v3 via app store. v3 was also pushed to the server. Manifest in app (v3) matched the one on the server (v3), so no remote update attempted. But the html files (and potentially other) files that were updated between v2 and v3 were not copied over into the cache correctly, so parts of the app were broken. The only way to fix was to go into the App settings and clear the cache, or completely uninstall and reinstall. Pushing a new v4 to the server and forcing a remote update might have worked as well, but I didn’t try it. None of which are great solutions for a consumer app.

In general it all just seems too prone to problems for my taste, so I’ve pulled it all out of my app for now. Let me know how you feel after you run it in production for a while, and maybe I’ll change my mind :slight_smile:

And re: PRs - I’m being completely serious when I say that all I know how to do in git is check code in. One of these days I’ll learn how to do your fancy forks and branches and pull requests and some such. :wink:

Hi

since i ve added the all the changes. ( Order of loading scripts, http interceptor for template files, imagecache, callback ) It works very well for me.

Tested about ~ 100 changes yesterday ( auto. via script ) and was working fine all the times.

( Tested on Nexus 4 ( Android 5.0.1 ) and IOS-SIM )

I ve modified the bootstrap.js to include just css/js and skip html ( html templates via angular http interceptor )

Today i will create a cordova-app-loader-angularjs-service to remove the “hacky” parts of my code and to get a reusable service which can be used by other applications

I will push the code to github when its done

Regards

Awesome, and kudos for working through all the issues. Looking forward to seeing it!

ok, tested this, looks like a good solution and seems to work great so far… Thanks!!

Thx for testing,

I m still working on a reusable component / service/ factory for angularjs based apps

@rajatrocks
I checked the app cache too
it looks like the html5 cache can not be used by the upcoming wkwebview

Yeah, I just saw that in Shazron’s post as well: https://shazronatadobe.wordpress.com/2015/03/03/wkwebview-and-apache-cordova/ - that’s unfortunate.

did you achieve to have a reusable component, whatever… ? I tried with the normal cordova-app-loader and with many hints from this topic and other places, but still don’t fully succeed.
would be cool to see a working example

Hi

sorry i can not provide a full example, since its a closed source app.

But i works like following

In my app.js i ve added a http interceptor in the config section

$httpProvider.interceptors.push(['UpdateService', function (UpdateService) {
                    return {
                        'request': function (config) {
                            if (UpdateService.isFileCached(config.url)) {

                                var url = UpdateService.getCachedUrl(config.url);
                                config.url = url;
                            }
                            return config;
                        },
                        'response': function (response) {
                            return response;
                        }
                    };
                }]);

The UpdateService looks like following

app.factory('UpdateService', ['$log', 'ConfigService', '$q', function ($log, ConfigService, $q) {


    var fs = new CordovaPromiseFS({
        Promise: Promise
    });

    var loader = new CordovaAppLoader({
        fs: fs,
        serverRoot: 'http://'+ConfigService.getHost()+':'+ConfigService.getPort()+'/update',
        localRoot: 'app',
        cacheBuster: true, // make sure we're not downloading cached files.
        checkTimeout: 10000, // timeout for the "check" function - when you loose internet connection

        manifest: 'manifest.json' + "?" + Date.now()
    });
    var service = {
        check: function () {

            var defer = $q.defer();
            loader.check().then(function (updateAvailable) {
                if (updateAvailable) {
                    defer.resolve(updateAvailable);
                }
                else {
                    defer.reject(updateAvailable);
                }
            });

            return defer.promise;
        },
        download: function (onprogress) {
            var defer = $q.defer();

            loader.download(onprogress).then(function (manifest) {
                defer.resolve(manifest);
            }, function (error) {
                defer.reject(error);
            });
            return defer.promise;
        },
        update: function (reload) {
            loader.update(reload);
        },
        isFileCached: function (file) {
            if (angular.isDefined(loader.cache)) {
                return loader.cache.isCached(file);
            }
            return false;
        },
        getCachedUrl : function (url) {
            if(angular.isDefined(loader.cache)) {
                return loader.cache.get(url);
            }
            return url;
        }



    };

    return service;
}]);

On App start ( and via a button ) the UpdateService gets invoked to check if an update is available

Hope this helps

Regards

@thetoaster was the example helpful or do you need further informations ?