Reputation: 353
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.
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