Runkeeper, among the best apps to track your runs using your phone’s GPS, takes location services on iOS and Android to their limit to optimize accuracy and usability. In this MBLTDev 2015 talk by Phillip Connaughton, learn how to get set up and running quickly, as well as great tips and tricks to improve your location tracking app. These include battery conservation tips, which GPS polling coordinates to ignore, and serious bugs to avoid to save you headaches and time 🏃🚀
Introduction (0:00)
I’m Phil Connaughton, a principal software engineer at Runkeeper for about four years. In my time at Runkeeper, I’ve worked on our iPhone app and our Android app, as well as helping to create the infrastructure that supports them. I’ve also worked most recently on the watchOS 2 app.
Overview (0:29)
I’ll give a brief intro about what Runkeeper is, then I’ll take you through a brief history of location tracking and some of the obstacles that Runkeeper has had to overcome along the way. We’ll start way back at iOS 2, and I’ll take you through iOS 9, and on to what we can do with location services on watchOS 2.
I will outline a few algorithms that Runkeeper uses to exclude inaccurate GPS points and take you through an example of how collecting GPS points can go terribly wrong. Lastly, I want to discuss what you can do to conserve battery life when you’re using location services since spinning up the radio does take up a lot of power and so just some things that we do to try and limit that.
About Runkeeper (1:34)
Runkeeper got started in 2008 as one of the first apps in the iOS store. We’re an inclusive fitness brand that isn’t tailored towards elite training and not really trying to help people win their local 5K; we’re more focused on on reaching the broader crowd and trying to just help people stay fit. Our motto is “Everyone. Every run.” We use location data in two distinct ways: during the activity and after the activity.
During the trip, we collect GPS points on the fly analyze them and determine if they’re accurate enough for us to use with the trip. Based on these points, we provide realtime feedback to the users in the form of audio cues and visual displays. We’ll show them their current distance, pace, burned calories, and more.
Additionally, we use broader location data for showing the general area that a user ran in, as well as regional advertisements. We also integrate with Spotify for region specific playlists.
Location Services History: iOS 2 (2:54)
Back in iOS 2, you couldn’t actually use location services in the background. As soon as your app was backgrounded we stopped receiving GPS points, so that posed a huge problem for us as we would have to message the users to keep the phone screen on. Otherwise, we wouldn’t be able to poll the location until the device is awake again.
Users weren’t too happy with that, and we weren’t too impressed with ourselves. Also, keeping the app in the foreground and the screen on crushes your battery life – even worse than GPS.
One of our users actually came up with a solution for us in the form of audio cues that fire once a minute. In iOS 2, that would keep our app from entering the background. This led to playing a silent audio file while you were running, allowing users to lock their screens with our app running in the foreground. This is definitely a bit of a hack.
iOS 3, 4, and 5 (4:38)
Here, Apple gave us a few new APIs including the region and heading APIs. Region monitoring is great. We actually don’t use it, but it can be handy. The operating system will actually launch your app from the background if a user enters or leaves a region.
iOS 6 (5:08)
The next major improvement to location services didn’t happen until iOS 6. That was the announcement of deferred updates. Deferred updates introduced developers with a good way to actually conserve battery life. You could get deferred updates with high-fidelity points. Prior to iOS 6, we had a function that would get a new location and it would show you what your old location was and this would fire every time that you got a point.
With deferred updates, we started getting an array of locations. To enable deferred updates, you just have to set your accuracy and your distance filter to none using kCLLocationAccuracyBest
and kCLDistanceFilterNone
on the following method:
-(void) allowDeferredLocationUpdatesUntilTraveled:(CLLocationDistance)distance
timeout:(NSTimeInterval)timeout
Just call on this method on your location manager, and you provide the distance and the time interval with it for when you want to receive GPS points, and it will basically turn off the radio for that period of time. This will conserve battery life because it doesn’t have to be on for as long.
iOS 7 (6:21)
During the launch of iOS 7, Apple came out with background refresh, which allowed apps to make calls in the background even when their app wasn’t on. You could piggy-back on other apps’ web calls and pull down the latest information. For example, if you had a social network, as soon as the user opens up your app all of that information would already be in the app and you wouldn’t have to make the web call yourself. This was great for developers.
Unfortunately, bloggers immediately told users to turn it off because it was killing their battery. With deferred updates off, the app’s functionality would devolve to iOS 3 accuracy, and many users unknowingly complained about it. It led us to this pretty ugly screen where if you turn off background refresh from settings, we notify users not to do that.
We were getting a ton of negative reviews from the previous experience, and launching this dialogue led to less of them.
iOS 8 (8:02)
In iOS 8, Apple added location service authorization so you can now have “always authorize” or “when-in-use authorization”. Always authorize is necessary for region monitoring and launching apps from the background, whereas when-in-use is great for just when you need it in the moment. We actually use when-in-use and the user’s service with two different dialogues based on which kind of location services you want. It’s far less intrusive to the user if you use the when-in-use.
iOS 9 & watchOS 2 (8:53)
That finally brings us to iOS 9 and watchOS 2. Not much excitement happened in iOS 9 – they added some new permissions so if you want to run in the background you have to set allow background location updates, but more exciting was watchOS 2, which added Core Location. The location data isn’t that great if you’re tracking with just the watch. We’re not going to be able to get fine grain location data because it doesn’t actually have a GPS chip in it. However, you can get general location so I believe it hits Wi-Fi or local Wi-Fi points, and will get you a general area from that.
Furthermore, watchOS 2 also only works when your app is in the foreground. So even if they did have a GPS chip in it, we wouldn’t be able to get the points anyways. As a result, what we’re gonna use it for, and this is kind of an early insight into our next release, is for the Apple Watch to track the general region that you were in, such as your neighborhood.
Setting Up Location Services (6:05)
Setting up location services is pretty easy. So this is it on iOS, you basically just have to register yourself as a delegate for location manager.
locationManager.delegate = self // Register your class to receive updates
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters // How close do we need to be?
locationManager.distanceFilter = kCLDistanceFilterNone // We will be informed of any movement
locationManager.requestWhenInUseAuthorization() // Add NSLocationWhenInUsageDescription to explain location usage
locationManager.startUpdatingLocation()
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { ... }
You have to set a desired accuracy, which is the radius of accuracy that you want your GPS points to be within. Distance filter tell the operating system to update you every 10 meters, every 50 meters, or just update me every single time you get a point. Then it’s requesting a when in use authorization in starting up location services.
As soon as you start using it, you’ll find out that location services is very noisy. This is the API that I showed earlier, that provides an array of locations. It’s noisy in the sense that it will give you tons of points that aren’t all very good.
A point has to pass a few tests before we’re willing to save it. We have a few basic filters that we require a point to pass before saving it. Those filters were created over time and have improved based on the many issues that we’ve run into.
GPS Point Filtering Algorithm (11:25)
Location services provides you with points that are within a certain degree of accuracy that you requested so while location services is starting up the points are actually more inaccurate than you might specify. So for tuning our algorithm we need a large set of data which means making many, many trips.
For debug builds, which are used primarily by Runkeeper employees, we store every single point that we get from location services to the local database. That ends up being pretty battery intensive so not only are we collecting all those points but we have more log messages that happen in those builds and then we’re actually spending all of that time writing to the database. For a six kilometer run, if you don’t filter out points, we’ll get about 2300 GPS points, versus about 600 points that we actually need to store for the release builds.
What do we see when we get a GPS point? You get a horizontal accuracy and that tells you within the radius of certainty. We throw out cell tower and Wi-Fi points which are inaccurate. We can tell that by looking at the vertical accuracy on a CLLocation that is not zero.
We also check the age. Sometimes GPS points come in out of date so we’ll get a point that will before the previous point that we got and that just seems weird to us, so we throw those out as well.
Next, we look at acceleration. What we’ll look for is if you’re running along in Moscow and then all of a sudden your next point is on the other part of the city, it’s probably not that accurate and probably not a point that we want to use, so we just have a simple check to see if you accelerated across the city like Superman.
Lastly, we check if the horizontal accuracy is less than zero. Apple, in their docs, say that any point that has a horizontal accuracy of less than zero is invalid. Not sure why they give it to us, but they do.
Reverse Geocoding (15:07)
We use GPS data for both fine-grained trips and for tracking general area. To find the name of the general area from GPS coordinates, you can use reverse geocoding on both iPhone and Android. It’s pretty simple to do:
iOS:
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(locations[0]) { (placemarks, error) -> Void in
if (placemarks?.count > 0) {
let placemark: CLPlacemark! = placemarks![0]
if (placemark.locality != nil && placemark.administrativeArea != nil) {
NSLog(String(format: "Locality: %@, Area: %@", placemark.locality!, placemark.administrativeArea!))
}
}
}
Android:
Geocoder geocoder = new Geocoder(context, Locale.getDefault());
try {
List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1);
if (addresses.size() > 0)
{
Address address = adressess.get(0);
if (address.getAdminArea() != null && address.getLocality() != null)
{
Log.d("Location Example", "Locality:" + address.getLocality() + " Area:" + address.getAdminArea());
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
In this Android code, we create a new geocoder, we pass in a longitude and latitude, and we’ll get back a list of addresses and then based on those, the list of addresses, you can actually show where the user is.
Advanced GPS-based Features & Bugs (15:40)
I will take you through a couple features we’ve implemented at Runkeeper, and some specifics on how they leverage location technologies as well as how inaccuracies of location services has caused issues.
AutoPause (15:55)
AutoPause is exactly as it sounds. It’s a feature that allows users to pause their activity without having to take any action. This came about because we don’t want users to be running along and then they get to an intersection and they have to mess with their armband as the thumb print doesn’t actually work if you’re trying to use it through the screen.
We’ll check to see if they’ve left a certain region. If a person hasn’t moved out of that region within a certain time interval, they’ve probably stopped, so we’ll automatically pause it for them.
Kalman filter path smoothing (16:53)
At Runkeeper, we use the Kalman filter, a common smoothing algorithm. What the Kalman filter does is analyzes a list of GPS points and adjust them to smooth them out. This is important because if you use the raw GPS data points, your run’s going to look like a jagged mess along the route that you ran, especially if you’re running between tall buildings, and the filter changes past points to handle that. We’re unable to do that because of features like AutoPause where we can’t adjust previous points because you don’t want to be pausing and unpausing a user’s run as you’re kind of filtering over their points.
The Android Shuffle bug (18:02)
We’ve run into several issues with GPS points over the years. Back in 2012, we ran into one that we like to refer to as the “Android Shuffle.” This one hit us pretty hard, and it was happening to about 0.1% of users. In it, the region that the person ran in would look like it was colored in by a kid, which would make no sense for the runners. When zoomed in, it appears like the jagged dots are connected to each other across the region.
The problem wasn’t with our sort function based on times, but instead it was the way we set our time stamps. It turns out that we were actually setting the time stamp on each GPS point as the System.currentTimeInMillis()
. It says not to do that right in the Android documentation.
It turns out that if a user is running near cell phone towers that for some reason have different time stamps associated with them, it will change the system clock. The current time in milliseconds uses the system’s clock, and we would associate those times with the GPS point. If you’re running near time zone boundaries and you’re constantly having your phone’s system time updated, it looks fine while you’re running, but when we go and try and sort the points by time stamp after your run, it looks like someone colored them in.
Elevation GPS data (20:52)
We actually don’t use the elevation on GPS points. We kind of made that decision back in like 2008 when we found it to be totally inaccurate. Don’t let me deter you from using it yourself but we just haven’t really tested it recently because we’re happy with the service that we use which is called Topocoding.
Presenting GPS data (21:18)
We spent a lot of time talking about the technical side of what we do for smoothing out data but GPS data just tends to bounce around and not be totally accurate. Some users would complain about this, and over time we discovered that the way that you present this data to the users can have a big impact.
To solve the issue of inaccurate-looking GPS points, we simply made the route line on the map wider. This may sound trivial, but we stopped getting complaints about it afterwards since people no longer interpreted the route line to be exact.
Preserving battery life (22:06)
GPS is a big killer of battery life, but some of the other big killers of battery life are things like network calls. While a user is going out for a run, we don’t allow for any network calls to happen from within our app with the exception of live tracking so that you can see where your friends are running while they’re running.
The other point here is that there are API methods that both Apple and Android provide where you notify the system when your app is entering the background and you don’t need to collect location services anymore. It’s really important to call on those methods.
Additionally, don’t call on location services if you don’t need it. For us, most users are using it to track a run, so as soon as they open up the app and land on our start screen, we kick up location services and we try to get pretty granular points. However, that might not be the case for you so if you want to limit your impact on battery life, I’d just use it whenever you need it.
Conclusion (23:26)
In conclusion, iOS and Android are both continuing to improve the ease in which to use location services, so with watchOS 2, you can use Health Kit to actually just get a distance that people are running so you don’t even need location services as much on watchOS 2. Then, next just consider the user’s perception when displaying their location data to them. It’s crazy that just drawing a thicker line can actually cause your app store reviews to get better. Lastly, I’d recommend to always be thinking about battery life.
Q&A (24:04)
Q: In Android and probably in iOS, a user can fake GPS data. Do you handle this case?
Phillip: No, we don’t handle that case since users would be hurting themselves. We would if were worried that people would fake it for Runkeeper competitions, but currently the prizes for those are pretty small and it hasn’t been an issue.
Q: Do use Core Motion framework to detect is user now walking or running or he just stopped or you use only GPS for location?
Phillip: We looked into it, but right now all of our activities are just labeled as a walk or a run without switching back and forth, so Core Location is enough for us.
Q: Couldn’t you just look at map data to try and smooth out a user’s run based on buildings in the area?
Phillip: We have looked into that in the past and we haven’t started doing it but it is a good idea. It would be a substantial amount of work I think and it only depends on how good the maps in the area are so we have a lot of runners who will run on trails and trails aren’t always in those maps.
Q: Who are the Runkeeper QA guys? Robots?
Phillip: They’re in very good shape and know the Boston area very well. 😉🏃
Receive news and updates from Realm straight to your inbox