Marko Vidalis
Marko Vidalis

Reputation: 353

CLLocationManager - iOS native module for background location updates causing 100% battery usage

I have created an iOS native module for my React Native application, which invokes the location manager service on iOS. The purpose of the app is to automatically detect when a user is driving in a vehicle, start recording their trip when they are driving for further processing.

In order to achieve this with background capabilities, I have implemented the location manager and the motion activity manager together. From the location manager, I am using both the startMonitoringSignificantLocation and startUpdatingLocations functions to retrieve the users location.

The significant locations function is used to launch the app in case the user force kills it, and the startUpdatingLocations function is then called to decrease the distance filter between location updates (The significant location updates alone are too inconsistent - There have been times where I would drive a few kilometres without receiving a location update if I was just using the significant location monitoring function).

It is working quite well now, however I've noticed that my app's battery usage is constantly high when the app is started (whether manually or by receiving a location update in the background) - even through the night when there are no locations being sent to the app by iOS. Here's a screenshot of the app's battery usage.

iOS Battery usage graph of app

The light blue bars are when the app had been started in the background with an update from the significant location monitor, and thereafter the startUpdatingLocations function was started.

My understanding is that if there are no location updates, my app is not given any background processing time and so I would expect that there would be no battery usage in those periods, and after I receive a location update then my app will contribute to battery usage for however long iOS allows it to process that location update. Is this correct? Or am I mistaken in my understanding?

When a location update comes through, the app automatically start listening for motion activity updates and this is stopped after a certain time interval. It's currently set to 5 minutes (which might be long and I will be decreasing it) - however it is still questionable that when the device is idle overnight, no location updates come through and so no motion activity updates will be coming through to the app either, but the battery usage is still maxed out.

I have looked into using the pausesLocationUpdatesAutomatically and activityType flags, however these also are not reliable and I lose a lot of driving data by the time that my app actually receives a location update and starts tracking the trip.

I'm not sure if this would be affecting battery usage, but an important thing to note is that as soon as a location/motion activity update is received in the native module (in the code below), it is sent using a EventEmitter and received by the React Native application for storage into cache and processing. Processing includes using the last +-20 location and motion activity updates in cache (anything older is discarded) and processing these data items in a basic algorithm to help me detect whether a user is in a vehicle or not.

here is the code that I am running in the native module:

import Foundation
import CoreMotion
import CoreLocation

@objc(ActivityRecognitionWrapper)
class ActivityRecognitionWrapper: RCTEventEmitter, CLLocationManagerDelegate{
    let motionManager = CMMotionActivityManager()
    var locationManager:CLLocationManager = CLLocationManager()
    var distanceFilter:Double = 50
    var updateDuration: Int = 300000
    var activityServiceRunning: Bool = false
    public static var shared:ActivityRecognitionWrapper?
    
    override init() {
        super.init()
        ActivityRecognitionWrapper.shared = self
    }
    

    @objc(enableActivityRecognitionTracking:withUpdateDuration:withResolver:withRejecter:) func enableActivityRecognitionTracking(_ distanceFilter: Double,updateDuration: Int, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {
        locationManager.delegate = self
        self.distanceFilter = distanceFilter
        self.updateDuration = updateDuration
        locationManager.allowsBackgroundLocationUpdates=true
        locationManager.distanceFilter = distanceFilter
        locationManager.activityType = .automotiveNavigation
        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        locationManager.requestAlwaysAuthorization()
        resolve(true)
    }

    
    @objc(getLastKnownLocation:reject:)
    func getLastKnownLocation(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void{
        debugPrint("Requested one location.")
        locationManager.requestLocation()
        resolve(true)
    }
    
    @objc(startSignificantLocationUpdates:reject:)
    func startSignificantLocationUpdates(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void{
        debugPrint("Starting significant location tracking")
        locationManager.allowsBackgroundLocationUpdates=true
        locationManager.pausesLocationUpdatesAutomatically = false
        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
        locationManager.startMonitoringSignificantLocationChanges()
        resolve(true)
    }
    
    @objc(startFrequentLocationUpdates:reject:)
    func startFrequentLocationUpdates(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void{
        debugPrint("Starting frequent location tracking")
        debugPrint("Distance Filter: \(distanceFilter)")
        locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        locationManager.pausesLocationUpdatesAutomatically = false
        locationManager.distanceFilter = distanceFilter
        locationManager.allowsBackgroundLocationUpdates=true
        locationManager.startUpdatingLocation()
        resolve(true)
    }
    
    @objc(stop:reject:)
    func stop(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void{
        debugPrint("Stopping all location tracking")
        locationManager.stopUpdatingLocation()
        locationManager.stopMonitoringSignificantLocationChanges()
        resolve(true)
    }
    
    func getConfidence(conf:CMMotionActivityConfidence?) -> String{
        if conf == CMMotionActivityConfidence.low {
            return "Low"
        } else if conf == CMMotionActivityConfidence.medium {
            return "Good"
        } else if conf == CMMotionActivityConfidence.high {
            return "High"
        } else {
            return "UNKNOWN"
        }
    }
    
    func getTransformedActivityObject(activity: CMMotionActivity, type:String) -> [String:String] {
        let dateFormatter = DateFormatter();
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS Z";
        return ["TIMESTAMP": dateFormatter.string(from: (activity.startDate)), "CONFIDENCE": getConfidence(conf: activity.confidence), "TYPE":type]
    }
    
    func getTransformedLocationObject(location: CLLocation) -> [String:String] {
        let dateFormatter = DateFormatter();
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS Z";
        if #available(iOS 13.4, *) {
            return ["TIMESTAMP": dateFormatter.string(from: (location.timestamp)),
                    "LAT":location.coordinate.latitude.description,
                    "LONG":location.coordinate.longitude.description,
                    "HORIZONTAL_ACCURACY":location.horizontalAccuracy.description,
                    "VERTICAL_ACCURACY":location.verticalAccuracy.description,
                    "SPEED_ACCURACY":location.speedAccuracy.description,
                    "SPEED":location.speed.description,
                    "ALTITUDE":location.altitude.description,
                    "COURSE":location.course.description,
                    "COURSE_ACCURACY":location.courseAccuracy.description
            ]
        } else {
            // Fallback on earlier versions
            //courseAccuracy not available pre ios13.4
            return ["TIMESTAMP": dateFormatter.string(from: (location.timestamp)),
                    "LAT":location.coordinate.latitude.description,
                    "LONG":location.coordinate.longitude.description,
                    "HORIZONTAL_ACCURACY":location.horizontalAccuracy.description,
                    "VERTICAL_ACCURACY":location.verticalAccuracy.description,
                    "SPEED_ACCURACY":location.speedAccuracy.description,
                    "SPEED":location.speed.description,
                    "ALTITUDE":location.altitude.description,
                    "COURSE":location.course.description,
            ]
        }
    }
    
    
    override func supportedEvents() -> [String]! {
        return ["ACTIVITY_UPDATE", "LOCATION_UPDATE", "RESET_ACTIVITY_RECOGNITION"]
    }
    
    
    @objc func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        debugPrint("Got status")
        switch status {
        case .denied: // Setting option: Never
            print("LocationManager didChangeAuthorization denied")
        case .notDetermined: // Setting option: Ask Next Time
            print("LocationManager didChangeAuthorization notDetermined")
        case .authorizedWhenInUse: // Setting option: While Using the App
            print("LocationManager didChangeAuthorization authorizedWhenInUse")
            // Stpe 6: Request a one-time location information
            locationManager.requestLocation()
        case .authorizedAlways: // Setting option: Always
            print("LocationManager didChangeAuthorization authorizedAlways")
            // Stpe 6: Request a one-time location information
            locationManager.requestLocation()
            debugPrint("Starting significant location tracking from authorization status change delegate")
            locationManager.allowsBackgroundLocationUpdates=true
            locationManager.pausesLocationUpdatesAutomatically = false
            locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
            locationManager.startMonitoringSignificantLocationChanges()
        case .restricted: // Restricted by parental control
            print("LocationManager didChangeAuthorization restricted")
        default:
            print("LocationManager didChangeAuthorization")
        }
    }
    
    func application(_ application:UIApplication, willFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey:Any]?) ->Bool{
        if let keys = launchOptions?.keys{
            if(keys.contains(.location)){
                locationManager = CLLocationManager()
                locationManager.delegate = self
                self.distanceFilter = 50
                self.updateDuration = 60000
                debugPrint("OS invoked package to deliver location in background")
                locationManager.allowsBackgroundLocationUpdates=true
                locationManager.distanceFilter = distanceFilter
                locationManager.activityType = .automotiveNavigation
                locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
                locationManager.requestAlwaysAuthorization()
                
            }
        }
        return true
    }
    
    @objc func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        locations.forEach { (location) in
            
            ActivityRecognitionWrapper.shared?.sendEvent(withName: "LOCATION_UPDATE", body: getTransformedLocationObject(location:location))
            if(CMMotionActivityManager.isActivityAvailable()){
                debugPrint("Starting motion updates for \(updateDuration) seconds" )
                if(!activityServiceRunning){
                    activityServiceRunning = true
                    motionManager.startActivityUpdates(to: OperationQueue.main)
                    { [self]
                        (activity) in
                        if (activity?.automotive)! {
                            ActivityRecognitionWrapper.shared?.sendEvent(withName: "ACTIVITY_UPDATE",  body: getTransformedActivityObject(activity: activity!, type: "IN_VEHICLE"))
                            //                        debugPrint("User using car")
                        }
                        if (activity?.cycling)! {
                            ActivityRecognitionWrapper.shared?.sendEvent(withName: "ACTIVITY_UPDATE", body: getTransformedActivityObject(activity: activity!, type: "CYCLING"))
                            //                        debugPrint("User is cycling")
                        }
                        if (activity?.running)!{
                            ActivityRecognitionWrapper.shared?.sendEvent(withName: "ACTIVITY_UPDATE", body: getTransformedActivityObject(activity: activity!, type: "RUNNING"))
                            //                        debugPrint("User is running")
                        }
                        if (activity?.walking)! {
                            ActivityRecognitionWrapper.shared?.sendEvent(withName: "ACTIVITY_UPDATE", body: getTransformedActivityObject(activity: activity!, type: "WALKING"))
                            //                        debugPrint("User is walking")
                        }
                        if (activity?.stationary)! {
                            ActivityRecognitionWrapper.shared?.sendEvent(withName: "ACTIVITY_UPDATE", body: getTransformedActivityObject(activity: activity!, type: "STILL"))
                            //                        debugPrint("User is stationary")
                        }
                        if (activity?.unknown)!{
                            ActivityRecognitionWrapper.shared?.sendEvent(withName: "ACTIVITY_UPDATE", body: getTransformedActivityObject(activity: activity!, type: "UNKNOWN"))
                            //                        debugPrint("Unknown activity")
                        }
                        
                        
                    }
                    DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.milliseconds(updateDuration)) { [self] in
                        motionManager.stopActivityUpdates()
                        debugPrint("Stopped motion updates after \(updateDuration) seconds" )
                        activityServiceRunning = false
                    }
                }
                
                
            }else {
                debugPrint("Motion not available")
            }
            
        }
    }
    
    @objc func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        debugPrint("LocationManager didFailWithError \(error.localizedDescription)")
        if let error = error as? CLError, error.code == .denied {
            // Location updates are not authorized.
            // To prevent forever looping of `didFailWithError` callback
            locationManager.stopMonitoringSignificantLocationChanges()
            locationManager.stopUpdatingLocation()

            return
        }
    }
    
}

I would really appreciate some assistance in changing/restructuring my code so that I can still achieve high-accuracy location and activity tracking without reporting so much battery use, especially during the idle times.

Thanks!

Upvotes: 1

Views: 517

Answers (0)

Related Questions