Riskometer – the covid-19 app that didn’t take off

This is a story of making an app called Riskometer. The aim of the app was to prevent coronavirus to spread. I will explain how the idea worked from the programmer point of view. Finally the app didn’t start off, because the polish government (which we were talking with for about 3 weeks) refused it, I’ll explain why in the later part.

 

In march 2020, in the time of the biggest lockdown, one of my colleges asked me to join the team that is working on the app for preventing coronavirus. In the team there was already about 30 people, but they didn’t have any programmers. I joined, cause I like interesting projects and I thought what bad can possibly happen. Maybe our app will save someone’s life.. That was worth the try.

 

The idea was simple .. in theory. Google is saving history of location of your mobile device if you have Google account connected – most people have. Now the app should take this data, and analyze it, comparing with location history of people that had covid-19 diagnosed. First, my job was to check out if it is even possible to do with the data that is provided by Google Takeout json files – the only way to get this location history. The big json file in the Takeout contains only GPS coordinates, timestamps and type of transport. The problem with this data was, the GPS coordinates are not accurate here. It can be checked if someone’s path crossed the path with someone infected, but with very bad accuracy – they can be on other side of a street for example. Also checking that someone crossed the path with the infected one was not the point at all. From the government, we’ve get restrictions, when we can assume that someone had a contact with COVID-19 infected person:

 

  • a person lives in the same house with someone infected by COVID-19
  • a person had direct physical contact with someone infected by COVID-19
  • a person had contact with someone infected by COVID-19 in less than two meters and more than 15 minutes 
  • a healthcare worker or another person that looks after someone infected by COVID-19
  • a person that was in the same plane in the distance of two places (each direction) with someone infected by COVID-19

 

We’ve focused on the point when you meet infected person for 15 minutes in less than 2 meters. That was not possible to check it just with the GPS coordinates from Google Takeout. Fortunately in the zip package, there was more files that we could use. There was a folder named “Semantic Location History” that contained JSON files for each month with a bit different data. There was more useful data including activities and visited places in the sequence of visited place -> activity -> visited place -> activity..

For example if you were in some place (house, shop, mall) or you’ve travelled from place A to place B indicating which type of transport did you use like car, bus, on foot etc. 

The most accurate result that we could get from the data that worked with the rule “2 meters for 15 minutes” was to detect if someone travelled in a car with infected one. That was very easy to check with this data. There is information about starting place, ending place, starting time, ending time and type of transport. If this parameters equals to the data of another person, that is very highly probable, that those two people was travelling in the same car, so the distance between them must have been less than two meters. 

Other options were to check if someone was working in the same place, if someone live in the same house, if someone was in a shop / barber / dentist etc. where worked someone infected. But they were not as accurate as travelling in the car, so it ended just as additional information in the app that can be shown.

 

In the first few days of the project we’ve asked volunteers on our Facebook group to get 100 Google Takeout json files (from 100 different people), for the demo. We’ve get them in 3 days, so we could start with creating the mvp. The app was a very simple php (symfony + propel) application that analyzed the json files and put them into the database. After you uploaded your Google Takeout zip file, It just checked if you travelled with someone infected (randomly at first) in a car, or have you been in same place and then displaying you a list of these activities.

 

The results in this mvp looked like this:

 

 

The main problem with the Google Takeout zip packages was providing them by the user. To download the package, user has to login into his Google account, navigate to takeout.google.com, from the list of available data select only location history and then click on export. Then he waits until the package is ready for download (may be a few minutes) and then finally he can download the takeout file. After the file is downloaded, he can upload it to the app, or extract the zip package and select json files from only last month. 

 

The process was too complicated for a regular person, we had to automate it a bit.  From Google there was no any API, or any other way to get this data. There was no way to automate it using just a web application. 

 

With another programmer who recently joined the team working on bluetooth-based module for the application, we came out with an idea of using WebView component to click-through the whole download process. The only thing that user had to do was to login into his Google Account, with all the steps running automatically in the background. Two of us (with some help in design and styling from other people), we’ve made a working mobile app in just one weekend. It worked as it was expected, the user opened the app, logged into his Google account, and after clicking the start button all the magic happened. In the hidden WebView component, the injected javascript code clicked on each button to finally download the file. After the app detected that the file is downloaded, it unpacked the zip and uploaded required json files to the server and displaying the result.

 

That was how the final version of the app looked like:

 

 

 

 

After presenting the app to the government they asked us how much it will be cost for servers for maintaining the server side code. That was really hard to guess. With a help of another programmer that is a specialist in AWS, we’ve came out with the maximum monthly price of €50k (for a whole nation using it, about 0.2 cents for person a month). After this the whole situation all started to be rough. Now the method of gaining the data from users was the problem (GDPR), there was problem that Google may be not able to maintain all the exports at the same time. Even some professor related to the government send an email to everyone involved, that the whole idea of the app is completely wrong, and he presented his research that the GPS coordinates are not accurate, because it can’t be compared to his professional gps. He didn’t even analyze the way of our app worked, but it didn’t matter. After that the whole project was down. The government said that they will pick some other project, and they did. They’ve chosen a company called Polidea that was making an open-source app called ProteGo Safe

Bonus: the JS code (hack) that was injecting into WebView to download Google Takeout JSON file with location history only:

(function(){
    var downloading = false;
    var debug = false;
    var uiDebug = false;
    var forceExport = false;
    var _exporting = false;
    var _v = 3;
    var dbg = null;
    var _alert = null;
    var _preloader = null;
    var _progressBar = null;
    try {
        var dbgEl = document.createElement("ul");
        dbgEl.setAttribute("class","rm-dbg");
        document.body.appendChild(dbgEl);
        dbg = txt => {
            console.log("dbg",txt);
            let li = document.createElement("li");
            li.innerHTML = txt;
            dbgEl.appendChild(li);
        };
        dbg(JSON.stringify([_v,debug,forceExport]));
        let style = document.createElement("link");
        style.setAttribute("rel","stylesheet");
        style.setAttribute("href","/style.css");
        document.head.appendChild(style);

        var _container = null;
        var getContainer = () => {
            if (null == _container) {
                _container = document.createElement("div");
                _container.setAttribute("class","rm-container "+(uiDebug?'rm-debug-mode':''));
                document.body.appendChild(_container);
                let cnt = 0;
                _container.addEventListener("click",e => {
                    if (cnt++ > 5) {
                        uiDebug = true;
                        _container.setAttribute("class","rm-container rm-debug-mode");
                        dbgEl.setAttribute("style","display: block !important");
                    }
                    if (_cnt > 10) {
                        _container.setAttribute("style","display: none !important");
                    }
                });
                _preloader = document.createElement("img");
                _preloader.src = "/Wedges-3s-200px.gif";
                _preloader.setAttribute("class","rm-preloader");
                _container.appendChild(_preloader);
                _alert = document.createElement("div");
                _alert.innerHTML = "";
                _alert.setAttribute("class","rm-alert");

                _container.appendChild(_alert);
                _progressBar = document.createElement("div");
                _progressBar.setAttribute("class","rm-progress-bar");
                _progressBar.appendChild(document.createElement("div"));
                _container.appendChild(_progressBar);
            }
            return _container;
        };

        var setAlert = (txt = "") => {
            if (null == _alert) {
                getContainer();
            }
            _alert.innerHTML = txt;
            return _alert;
        };

        var setProgressBar = (progress) => {
            if (null == _progressBar) {
                getContainer();
            }
            _progressBar.childNodes[0].setAttribute("style", "width: "+progress+"%");
            _progressBar.childNodes[0].innerHTML = ""+progress+"%";
            return _progressBar;
        };

        var tryLang = (versions,operation) => {
            let result = false;
            versions.forEach(version => {
                try {
                    operation(version);
                    result = true;
                } catch (e) {
                }
            });
            return result;
        };
        var setAlertFinish = () => setAlert("Eksport rozpoczęty. Po otrzymaniu jego ukończeniu dostaniesz powiadomienie na e-mail od Google.");

        var downloadDaemon = () => {
            if (!download()) {
                setTimeout(()=>downloadDaemon(),5000);
            }
        };

        var finish = () => {
            setAlertFinish();
            setProgressBar(100);
            downloadDaemon();
        };


        var doExport = () => {
            setAlert("Trwa automatyczny eksport, proszę nie zamykać okna..");
            setProgressBar(5);

            dbg("deselect all");

            if (!tryLang(["Odznacz wszystkie","Deselect all"],version =>  document.querySelector('[aria-label="'+version+'"]').click())) {
                dbg("deselect not found");
                return false;
            }




            setTimeout(() => {

                setProgressBar(20);
                dbg("Select Location History");

                tryLang(["Wybierz Historia lokalizacji","Select Location History"],version => document.querySelector('[aria-label="'+version+'"]').click());

                setTimeout(() => {


                    setProgressBar(30);
                    dbg("Next step");

                    tryLang(["Następny krok","Next step"],version => document.querySelector('[aria-label="'+version+'"]').click());

                    setTimeout(function () {
                        dbg("temp1");
                        setProgressBar(40);
                        document.querySelector("[data-value='TEMP']").click();
                        setTimeout(() => {
                            dbg("temp2");
                            setProgressBar(50);
                            document.querySelectorAll("[data-value='TEMP']")[1].click();

                            setTimeout(() => document.querySelectorAll('button').forEach(el => {
                                setProgressBar(60);
                                if (-1 != ["Utwórz eksport","Create export"].indexOf(el.textContent)) {
                                    dbg("create export");
                                    if (!debug) {
                                        setProgressBar(80);
                                        el.click();

                                    }
                                    setTimeout(() => {
                                        finish();
                                    },800);
                                }
                            }), 800);
                        }, 800);
                    }, 800);
                }, 800);
            }, 800);
            return true;
        };

        var download = () => {



            var dateTest = ((txt) => {
                var res = /^(\d+)\s(\w+)(\s)(\d+): (Historia lokalizacji|Location History)$/.exec(txt);
                if (null == res) {
                    res = /^Utworzone:\s(\d+)\s(\w+)(\s)(\d+)$/.exec(txt);
                    if (null == res) {
                        var enRes = /^Created on:\s(\w+)\s(\d+),\s(\d+)$/.exec(txt);
                        if (null != enRes) {
                            res = [enRes[0],enRes[2],enRes[1],"",enRes[3]];
                        } else {
                            enRes = /^Location History on\s(\w+)\s(\d+),\s(\d+)$/.exec(txt);
                            if (null != enRes) {
                                res = [enRes[0],enRes[2],enRes[1],"",enRes[3]];
                            }
                        }
                    }
                }
                return null != res && (new Date().getDate() == parseInt(res[1]) || new Date().getDate() == parseInt(res[1])+1)
                    && new Date().getMonth() == {"marca": 2,"kwietnia": 3,"March": 2,"April": 3}[res[2]]
                    && new Date().getFullYear() == parseInt(res[4])
            });
            document.querySelectorAll("a").forEach(obj => {
                if (/^takeout\/download/.test(obj.getAttribute("href"))) {
                    [obj
                        .parentNode
                        .parentNode
                        .parentNode
                        .parentNode
                        .parentNode
                        .parentNode
                        .firstChild,obj
                        .parentNode
                        .parentNode
                        .parentNode
                        .parentNode
                        .childNodes[3],
                        obj
                            .parentNode
                            .parentNode
                            .parentNode
                            .parentNode
                            .childNodes[3]
                    ].forEach(date => {
                        console.log(date);
                        if (null != date && dateTest(date.textContent.trim()) && !downloading) {

                            dbg("download "+date.textContent.trim()+" "+obj.getAttribute("href"));
                            if (!debug) {
                                document.location.href = obj.getAttribute("href");
                            }
                            downloading = true;
                        }
                    });


                }
            } );
            if (downloading) {
                dbg("return downloading");
                return true;
            } else {
                return false;
            }
        };



        var run = () => {
            document.querySelectorAll("span").forEach(obj => {
                if (-1 != ["Anuluj eksportowanie","Cancel export"].indexOf(obj.textContent.trim())) {
                    _exporting = true;
                }
            });
            if (_exporting) {
                dbg("already exporting");
                finish();
                return;
            }

            if (!forceExport && download()) {
                setAlert("Pobieranie pliku rozpoczęte...");
            } else {
                if (!doExport()) {
                    dbg("not exporting");
                    getContainer().setAttribute("style","display: none");
                    dbgEl.setAttribute("style","display: block !important");
                }
            }
        };
        run();


    } catch (e) {
        console.error("FAILED",e);
        if (null != dbg) {
            dbg("ERROR "+e.toString());
        }
        setAlert("Error: "+e.toString());
    }

})();