Using the MOPRIM TMD SDK

Table of content

Initialization

The SDK requires an API key and an API enpoint configuration. It is recommended to configure the TMD in your AppDelegate’s didFinishLaunchingWithOptions method with a call to TMD.initWithKey:withEndpoint:withLaunchOptions:. If for some reason you cannot initialize the TMD from the AppDelegate, be sure to call TMD.initWithKey:withEndpoint:withLaunchOptions: before you do any other operation with the SDK.

import MOPRIMTmdSdk

(...)

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    // Configure the app to trigger Background Fetch events as regularly as possible.
    UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
    
    // Declare your API Key and Endpoint:
    let myKey = "MY_SDK_CONFIG_KEY"
    let myEndpoint = "MY_SDK_CONFIG_END_POINT"
            
    // Initialize the TMD:
    TMD.initWithKey(myKey, withEndpoint: myEndpoint, withLaunchOptions: launchOptions).continueWith { (task) -> Any? in
            if let error = task.error {
                NSLog("Error while initializing the TMD SDK: %@", error.localizedDescription)
            }
            else {
                // Get the app's installation id:
                NSLog("Successfully initialized the TMD with id %@", task.result ?? "<nil>")
            }
            return task;
    }
    return true
}
    

Additionally, the TMD SDK’s backgroundFetch method needs to be called from your AppDelegate’s performFetchWithCompletionHandler method in order to send the recorded data from the phone to MOPRIM’s API when the app is in the background:

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    // Run our background operations
    TMD.backgroundFetch().continueWith (block: { (task) -> Void in
        let tmdFetchResult:UIBackgroundFetchResult = UIBackgroundFetchResult(rawValue: (task.result!.uintValue))!
		// Call the completion handler with the UIBackgroundFetchResult returned by TMD.backgroundFetch(), or with your own background fetch result
        completionHandler(tmdFetchResult)
    })
}

To ensure that data is sent correctly to MOPRIM’s API, you should also call TMD.application:handleEventsForBackgroundURLSession:completionHandler: from your AppDelegate’s:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    TMD.application(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler)
}

It is also recommended that you make a call to the TMD’s applicationWillTerminate method from your AppDelegate’s applicationWillTerminate method in order to give a chance to the TMD to stop gracefully when the application is killed.

func applicationWillTerminate(_ application: UIApplication) {
    TMD.applicationWillTerminate()
}

Starting the TMD service

The TMD service can be started with:

TMD.start()

The TMD SDK will take care of asking user authorization to access location and motion data when it is first started. However, if you want to have control over when these permissions are asked to the user and what happens when they are denied, we recommend that you ask these permissions before starting the TMD for the first time.

Asking for permission to access location data

In your code, you can do that with an instance of CLLocationManager:

import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {
    let locationManager = CLLocationManager()
    
    func askLocationPermissions() {
        self.locationManager.delegate = self
        self.locationManager.requestAlwaysAuthorization()
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if #available(iOS 14.0, *) {
            let preciseLocationAuthorized = (manager.accuracyAuthorization == .fullAccuracy)
            if preciseLocationAuthorized == false {
                manager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: "tmd.AccurateLocationPurpose")
                // Note that this will only ask for TEMPORARY precise location.
                // You should make sure to ask your user to keep the Precise Location turned on in the Settings.
            }
        } else {
            // No need to ask for precise location before iOS 14
        }
    }
}

Note that starting from iOS 13, when requesting to access the location “Always”, iOS first asks the user for the “WhenInUse” authorization, and later, at a time that it deems appropriate, for the “Always” authorization. Starting from iOS 13.4, it is possible, however, to first request the “WhenInUse” authorization, and then immediately request the “Always” authorization, in order to go around this limitation. The Transport Mode Detection will only work once the user allows the location to be accessed “Always”. You can read this stackoverflow answer for more detailed info about this situation.

If you want the user to allow the location to be accessed “Always” as soon as possible, here are some recommendations that can be used, depending on the iOS version:

  • Before iOS 13: Request “Always” Authorization.
  • iOS 13 to 13.3: Request “When In Use” Authorization and ask user to go to Settings to change to “Always”.
  • iOS 13.4 and later: Request “When In Use” Authorization, and then immediately request “Always” Authorization.

Here is an example that follows these recommandations. You can also find this code in the TMD Sample Project.


import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

    var locationManager : CLLocationManager?
    var didAskWhenInUseLocationAccess = false
    var didAskAlwaysLocationAccess = false

    @IBAction func requestLocationAccess(_ sender: Any) {
        if (self.locationManager == nil){
            self.locationManager = CLLocationManager()
            self.locationManager?.delegate = self
        }
        if #available(iOS 13.0, *) {
            // First we ask for "When in use", then we ask for "Always".
            // Why we do that instead of asking straight for Always:
            //     If we were to ask first for "Always",
            //     the user will first be prompted to allow access "When in use",
            //     and we cannot control when the user will be prompted for "Always".
            //     By asking for "When in use" and then for "Always", we control when "Always" is asked.
            
            if didAskWhenInUseLocationAccess == false {
                self.locationManager!.requestWhenInUseAuthorization()
                didAskWhenInUseLocationAccess = true
            }
            else {
                self.locationManager!.requestAlwaysAuthorization()
                didAskAlwaysLocationAccess = true
            }
        }
        else {
            // Before iOS 13, we can directly ask for "Always"
            self.locationManager!.requestAlwaysAuthorization()
            didAskAlwaysLocationAccess = true
        }
    }
    
    // Should be used on iOS 14 and later versions:
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if #available(iOS 14.0, *) {
            let status = manager.authorizationStatus
            didUpdateLocationAuthorizationStatus(authorizationStatus: status, precise: (manager.accuracyAuthorization == .fullAccuracy))
        }
    }
    
    // Should be used on iOS 13 and earlier versions:
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        didUpdateLocationAuthorizationStatus(authorizationStatus: status, precise: true)
    }
    
    func didUpdateLocationAuthorizationStatus(authorizationStatus: CLAuthorizationStatus, precise:Bool){
        
        if (authorizationStatus == .notDetermined) {
            NSLog("locationManager.didChangeAuthorization:notDetermined")
        }
        else if (authorizationStatus == .authorizedAlways){
            NSLog("locationManager.didChangeAuthorization:authorizedAlways")
            requestLocationButton.isEnabled = false
            if (!precise){
                let alert = UIAlertController.init(title: "Now please go to Settings and allow Precise location access.",
                                                   message: "Settings > TMD Sample Project > Location > Precise Location",
                                                   preferredStyle: .alert)
                alert.addAction(UIAlertAction.init(title: NSLocalizedString("Go to Settings", comment: ""),
                                                   style: .default,
                                                   handler: {(alert: UIAlertAction!) in
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                }))
                self.present(alert, animated: true, completion: nil)            }
            else {
                requestLocationButton.setTitle("Location Access Granted!", for: .normal)
            }
        }
        else if (authorizationStatus == .denied || authorizationStatus == .restricted) {
            NSLog("locationManager.didChangeAuthorization:restricted")
        }
        else {
            NSLog("locationManager.didChangeAuthorization:\(authorizationStatus.rawValue)")
            if #available(iOS 13.4, *) {
                if (authorizationStatus == .authorizedWhenInUse){
                    // Here, we can ask the user to "Change to Always Allow".
                    // Note that requestAlwaysAuthorization() will do nothing if the user previously went to the settings to change the permission
                    // And there is no way to know wether the user did that or not.
                    self.locationManager!.requestAlwaysAuthorization()
                    didAskAlwaysLocationAccess = true
                }
            }
            else {
                let alert = UIAlertController.init(title: "Now please go to Settings and allow location access Always.",
                                                   message: "Settings > TMD Sample Project > Location > Always",
                                                   preferredStyle: .alert)
                alert.addAction(UIAlertAction.init(title: NSLocalizedString("Go to Settings", comment: ""),
                                                   style: .default,
                                                   handler: {(alert: UIAlertAction!) in
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                }))
                self.present(alert, animated: true, completion: nil)
            }
        }
    }
}

Asking for permission to access motion data

In your code, using an instance of CMMotionActivityManager to start activity updates will prompt the user to allow motion access, like that:

import CoreMotion

let motionActivityManager = CMMotionActivityManager()
func askMotionPermissions() {
    if CMMotionActivityManager.isActivityAvailable() {
        self.motionActivityManager.startActivityUpdates(to: OperationQueue.main) { (motion) in
            print("received motion activity")
            self.motionActivityManager.stopActivityUpdates()
        }
    }
}

Stopping the TMD service

You can stop the TMD service with:

TMD.stop()

TMD lifecycle

Applications behave differently when they are in foreground and in background, and your app might get killed by the OS after being in the background for a while. If configured properly, your app can stay active in the background to some extent, and wake up to detect that the user started moving, and start the TMD.

In order to keep the TMD running in the background, the app must have access to location and motion data, and register to the “fetch” and “location” background modes, as mentionned in the Configure your project section.

Knowing if the TMD started properly

You can register a delegate of type TMDDelegate to be notified when the TMD starts and stops, or when it could not start or had to stop because of an error.

Fetching metadata about the TMD results on the Cloud

TMD metadata contains information about the information that our cloud knows about a user:

  • The first timestamp (in milliseconds) that we received data (i.e., start of the first recognized activity)
  • The last timestamp (in milliseconds) that we received data
  • The last timestamp of an uploaded TMD activity
  • The last timestamp of an uploaded location
  • The last timestamp where TMD was synced with the cloud
  • The last timestamp where location was synced with the cloud
  • The average daily co2 value for the community
  • The average daily distance value for the community
  • The average daily duration value for the community

More information about TMDCloudMetadata can be found in the appledoc.

In order to get the TMD Cloud metadata, please run the TMDCloudApi.fetchMetadata method.

TMDCloudApi.fetchMetadata().continueWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("fetchMetadata Error: %@", error.localizedDescription)
		}
		else if let metadata = task.result {
		    NSLog("fetchMetadata result: %@", metadata)
		}
		return nil;
	}
}

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand and an available internet connection.

The TMD SDK uses TMDTask to handle operations with the TMDCloudAPI. More information can be found in the appledoc

Fetching data from the MOPRIM Cloud

TMD Activities contains information about the recognized means of transport of the mobile phone carrier:

  • The unique id of the activity when stored in the local cache
  • The timestamp (in milliseconds) when the activity was last fetched from the MOPRIM Cloud
  • The timestamp (in milliseconds) when the activity started
  • The timestamp (in milliseconds) when the activity stopped
  • The label for the corrected activity if the activity was changed by the user manually
  • The label for the original TMD activity
  • A boolean indicating if the activity has been corrected by the user
  • The last update timestamp (in milliseconds) of the activity
  • The C02 value (in grams) for the activity
  • The distance (in meters) covered during the activity
  • The average speed (in km/h) during the activity
  • The encoded polyline of the locations crossed during the activity
  • The origin of the activity, i.e., label of the origin location or the location for stationary
  • The destination of the activity
  • A sync flag to check whether change made manually on the activity have been synced with the cloud

More information about TMDActivity can be found in the appledoc.

In order to get the list of TMD activities, please run the TMDCloudApi.fetchData:minutesOffset: method or alike.

TMDCloudApi.fetchData(date, minutesOffset: 0).continueWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("fetchData Error: %@", error.localizedDescription)
		}
		else if let data = task.result {
		    NSLog("fetchData result: %@", data)
		}
		return nil;
	}
}

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand and an available internet connection.

Manually correcting an activity label

In the event the TMD recognized a wrong activity or the discovered activity is not precise enough (e.g., motorized/road/car instead of motorized/road/car/electric for an electric car), it is possible to manually change the label of this activity by using the TMDCloudApi.correctActivity:withLabel: method.

TMDCloudApi.correctActivity(activity, withLabel: newLabel).continueOnSuccessWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("correctActivity Error: %@", error.localizedDescription)
		}
		else if let data = task.result {
		    NSLog("correctActivity result: %@", data)
		}
		return nil;
	}
}

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand.

The iOS SDK is configured via the MOPRIM Cloud. Therefore, the number of TMD activities that can be recognized (e.g., motorized/road/car, non-motorized/bicycle) can change everytime the TMD is reconfigured with the latest models. Please refer to the list of current activities.

Annotating an activity (i.e., add metadata)

The TMD SDK enables you to annotate an activity with more information than the one provided by the SDK natively. You can set the metadata for this activity by using the TMDCloudApi.annotateActivity method.

TMDCloudApi.annotateActivity(activity, withMetadata: metadata).continueOnSuccessWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("annotateActivity Error: %@", error.localizedDescription)
		}
		else if let data = task.result {
		    NSLog("annotateActivity result: %@", data)
		}
		return nil;
	}
}

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand.

It is possible to update the label and add metadata at the same time by using the TMDCloudApi.updateActivity:withLabel:withMetadata: method.

Fetching trips from the MOPRIM Cloud

TMD Trips are objects that group TMD Activities together as legs when the Moprim Cloud has determined that they are part of the same trip.

A TMDTrip contains the following:

  • The unique id of the trip when stored in the local cache
  • The timestamp (in milliseconds) when the trip was last fetched from the MOPRIM Cloud
  • The timestamp (in milliseconds) when the trip was last modified
  • The timestamp (in milliseconds) when the trip started
  • The timestamp (in milliseconds) when the trip stopped
  • The name of the main activity (mainMode) of the trip
  • The list of activities (legs) of the trip
  • A boolean indicating if the trip has been validated by the user
  • A boolean indicating if the trip is considered “completed” by the Moprim Cloud
  • The C02 value (in grams) for the trip
  • The distance (in meters) covered during the trip
  • The origin of the trip, i.e., label of the origin location or the location for stationary
  • The destination of the trip
  • A sync flag to check whether change made manually on the trip have been synced with the cloud

More information about TMDTrip can be found in the appledoc.

Similarly to TMD Activities, it is possible to fetch TMD Trip data from the cloud or from the cache, with the following methods:

Fetching TMD Trips will also fetch their TMD Activities.

Here is an example of fetching trips for a date:

TMDCloudApi.fetchTrips(for: date, minutesOffset:0).continueWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("fetchTrips Error: %@", error.localizedDescription)
		}
		else if let data = task.result {
		    NSLog("fetchTrips result: %@", data)
		}
		return nil;
	}
}

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand and an available internet connection.

Validating a trip

Validating a trip will mark it and its activities (or legs) as validated. This can be done by calling the following method: TMDCloudApi.validateTrip:

TMDCloudApi.validateTrip(trip).continueOnSuccessWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("validateTrip Error: %@", error.localizedDescription)
		}
		else if let data = task.result {
		    NSLog("validateTrip result: %@", data)
		}
		return nil;
	}
}

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand.

Annotating a trip (i.e., add metadata)

A trip can be updated with some metadata, or a reason for the trip. This can be done by calling the following method: TMDCloudApi.updateTrip:withReason:withMetadata:

TMDCloudApi.updateTrip(trip, withReason: "errands", withMetadata: metadata).continueOnSuccessWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("updateTrip Error: %@", error.localizedDescription)
		}
		else if let data = task.result {
		    NSLog("updateTrip result: %@", data)
		}
		return nil;
	}
}

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand.

Fetching statistics about the user mobility

TMD statistics contain information about a list of statistics for each type of activity and for a specific time period (weekend vs. weekdays and last X days vs. overall. For each value we provide the combined sum of CO2 emissions, the distance and duration of these activities for the user as well as for the community (daily average per user).

Check the appledoc for more information.

TMDCloudApi.fetchStatsForLast(nbOfDays).continueOnSuccessWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("fetchStats Error: %@", error.localizedDescription)
		}
		else if let data = task.result {
		    NSLog("fetchStats result: %@", data)
		}
		return nil;
    }
}

Check out the appledoc appledoc for more information about TMDStats and TMDStatsValue.

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand and an available internet connection.

Allowing the SDK to perform uploads using Mobile Data

The TMD SDK has an option to enable or disable the upload of mobility data to our cloud using Mobile Data. When this option is disabled, uploads will only happen over Wifi.

This option is disabled by default. You should therefore call the following method (after initializing the TMD for example), if you want uploads to be done using Mobile Data as well:

TMD.setAllowUploadOnCellularNetwork(true)

Forcing the upload of TMD data to the MOPRIM cloud

Our system usually uploads the data automatically to our cloud periodically (when the OS triggers a background fetch, or when the service is started), but it is possible for the application developer to force the upload of data to our cloud.

The current modality is never synchronized with the cloud as it is not considered as completed.

To force the upload of data to our cloud, use the following method:

TMDCloudApi.uploadData().continueWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("uploadData Error: %@", error.localizedDescription)
		}
		else if let metadata = task.result {
		    NSLog("uploadData success: %@", metadata)
		}
		return nil;
    }
}

TMDUploadMetadata contains information about timestamp and number of locations uploaded to the Cloud with the call as well as the information for TMD activities.

For more information about TMDUploadMetadata, check out the appledoc.

This method and its completion block execute in a background thread, and require the TMD to be initialized beforehand and an available internet connection.

You can also register a delegate of type TMDUploadDelegate to be notified when an upload starts or ends with the method:

TMD.setUploadDelegate(delegate)

Or at upload time with:

TMDCloudApi.uploadDataWithDelegate(delegate).continueWith { (task) -> Any? in
	DispatchQueue.main.async {
		// Execute your UI related code on the main thread	
		if let error = task.error {
		    NSLog("uploadData Error: %@", error.localizedDescription)
		}
		else if let metadata = task.result {
		    NSLog("uploadData success: %@", metadata)
		}
		return nil;
    }
}

For more information about TMDUploadDelegate, check out the appledoc.

Allowing automatic download of new data

It is possible to allow the TMD SDK to automatically download new data after the Moprim Cloud has received and analyzed newly uploaded data. To do so, call this method before starting the TMD:

TMD.setAllowAutoFetch(true)

The auto-fetch is disabled by default. You can use TMD.isAutoFetchAllowed() to check its value.

Registering for notifications on updated data

It is possible to observe NSNotifications that will happen whenever TMDTrip or TMDActivity objects have been changed in the cache. The names of the NSNotifications and the keys that can be used with their corresponding userInfo dictionary can be accessed from TMDNotifications. For more information about TMDNotifications, check out the appledoc. Here is an example of registering to notifications to print in the console what has been changed in the cache:

func registerToNotifications(){
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.didUpdateActivities(notification:)),
                                           name: NSNotification.Name(rawValue: TMDNotifications.didUpdateActivitiesNotificationName()),
                                           object: nil)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.didUpdateTrips(notification:)),
                                           name: NSNotification.Name(rawValue: TMDNotifications.didUpdateTripsNotificationName()),
                                           object: nil)
}

@objc func didUpdateActivities(notification: Notification){
    if let dict = notification.userInfo {
        if let inserted = dict[TMDNotifications.insertedActivitiesKey()] as? [TMDActivity] {
            NSLog("didUpdateActivities.inserted \(inserted.count) activities")
            for activity in inserted {
                NSLog("didUpdateActivities.inserted:\(activity.activityId) - \(activity.activity())")
            }
        }
        if let updated = dict[TMDNotifications.updatedActivitiesKey()] as? [TMDActivity] {
            NSLog("didUpdateActivities.updated \(updated.count) activities")
            for activity in updated {
                NSLog("didUpdateActivities.updated:\(activity.activityId) - \(activity.activity())")
            }
        }
        if let deleted = dict[TMDNotifications.deletedActivitiesKey()] as? [TMDActivity] {
            NSLog("didUpdateActivities.deleted \(deleted.count) activities")
            for activity in deleted {
                NSLog("didUpdateActivities.deleted:\(activity.activityId) - \(activity.activity())")
            }
        }
    }
}

@objc func didUpdateTrips(notification: Notification){
    if let dict = notification.userInfo {
        if let inserted = dict[TMDNotifications.insertedTripsKey()] as? [TMDTripNotification] {
            NSLog("didUpdateTrips.inserted \(inserted.count) trips")
            for trip in inserted {
                NSLog("didUpdateTrips.inserted:\(trip.tripId) - \(trip.timestampStart) - \(trip.timestampEnd)")
            }
        }
        if let updated = dict[TMDNotifications.updatedTripsKey()] as? [TMDTripNotification] {
            NSLog("didUpdateTrips.updated \(updated.count) trips")
            for trip in updated {
                NSLog("didUpdateTrips.updated:\(trip.tripId) - \(trip.timestampStart) - \(trip.timestampEnd)")
            }
        }
        if let deleted = dict[TMDNotifications.deletedTripsKey()] as? [TMDTripNotification] {
            NSLog("didUpdateTrips.deleted \(deleted.count) trips")
            for trip in deleted {
                NSLog("didUpdateTrips.deleted:\(trip.tripId) - \(trip.timestampStart) - \(trip.timestampEnd)")
            }
        }
    }
}

Possible errors returned by the MOPRIM TMD SDK methods

The list of possible errors returned by any TMD method is available in the appledoc