React-Native Integration of Motion-S iOS and Android SDKs

Prerequisites

We distribute our SDKs as .aar for Android and .xcframework for iOS. For our example setup, we use Kotlin and Swift as native platform languages. Using Java and Objective C instead is of course also possible.

This documentation is divided into three parts and will assist you in adding the SDKs into a blank react-native app. You will receive an already set up example project alongside this document.

The Structure of the document will be as follows:

1. Integration of the SDKs
1.1 Android
1.2 iOS
2. Interfacing the SDKs on the React side

1. Integration of the SDKs

1.1 Android

Adding the android .aar library to your React-native project

Create a folder called libs in the root directory of the android part of your project and place the .aar
library inside.
Next, you need to tell Gradle to import the library into your project:

  1. In the top-level build.gradle add the following lines:
allprojects {
    repositories {
       
        flatDir {
            dirs project(':app').file('libs')
        }
    }
}
  1. Add the dependency to the list of dependencies in your module build.gradle:
implementation(name: 'motionssdk-release', ext: 'aar')

Add needed dependencies for the library

Unfortunately, the .aar library contains only the classes of the SDK itself, so the dependencies needed for the SDK to work, have to be imported on your side. The module build.gradle has the following dependencies:

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"  
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  
implementation 'androidx.core:core-ktx:1.3.2'  
implementation 'androidx.appcompat:appcompat:1.2.0'  
implementation 'com.google.android.material:material:1.3.0'  
 
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")  
implementation("io.reactivex.rxjava2:rxkotlin:2.3.0")  
implementation("com.google.firebase:firebase-analytics:18.0.3")  
implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")  
implementation("com.google.android.gms:play-services-location:+")  
implementation("com.google.android.gms:play-services-location:17.0.0")  
implementation("com.google.code.gson:gson:2.8.5")  
implementation("androidx.work:work-runtime:2.3.4")  
implementation("androidx.work:work-rxjava2:2.3.4")  
implementation("androidx.security:security-crypto:1.0.0-rc03")  
implementation("com.cossacklabs.com:themis:+")  
implementation("com.squareup.retrofit2:retrofit:2.9.0")  
implementation("com.squareup.retrofit2:converter-gson:2.9.0")  
implementation("com.squareup.retrofit2:adapter-rxjava2:2.9.0")  
implementation("com.squareup.okhttp3:logging-interceptor:4.7.2")  
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")  
implementation("androidx.room:room-runtime:2.2.3")  
implementation("androidx.room:room-ktx:2.2.3")  
implementation("com.jakewharton.threetenabp:threetenabp:1.2.3")  
implementation("org.conscrypt:conscrypt-android:2.2.1")  
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.31")  
implementation("androidx.room:room-rxjava2:2.2.3")

Add Permissions to Manifest

The SDK needs the following permissions. Add them to your manifest:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />  
<uses-permission android:name="android.permission.BLUETOOTH" />  
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />  
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />  
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />  
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />  
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />  
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />  
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />  
<uses-permission android:name="android.permission.INTERNET" />  
<uses-permission android:name="android.permission.WAKE_LOCK" />

Add String values and Motion-S.xml

Add the following string resources and replace the values if you wish.

  <resources>
    <string name="app_name">SdkReactIntegration</string>
    <string name="ok_button">OK</string>
    <string name="permissions_message">Grant the necessary permissions for the application to work properly</string>
    <string name="permissions_action">Grant</string>
    <string name="permissions_denied">Permission Denied: Grant the necessary permissions for the application to work properly</string>
    <string name="location_permission_title">Location Permission</string>
    <string name="location_permission_message">Trip Recorder app automatically records your driving to provide you insights into your trips. For this to work we require to collect location data even when the app is closed or not in use.</string>
    <string name="gps_status">Enable either GPS or any other location service to find current location.  Click OK to go to location services settings to let you do so.</string>
</resources>

Place a file called MotionS.xml into the values folder with the following content. The file is needed to initialize the SDK with a default configuration. You can change the settings during runtime if you wish but the SDK won't start without the MotionS.xml file.

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <string name="apiKey">your_apiKey_here</string>
    <string name="motions_sdk">1.0.0</string>
    <string name="url">https://example.com/</string>
    <string name="activityUrl">https://example.com/a</string>
    <string name="baseUrl">https://api.motion-s.com/</string>
    <bool name="customSettings">true</bool>
    <string name="token">token</string>
    <string name="version">app_version</string>
    <string name="customKey"></string>
    <string name="customValue"></string>
    <bool name="activityTransition">false</bool>
</resources>

We will provide you with values for the URL and apiKey. For testing purposes you can point the SDK to some endpoint of yours to log the requests that are coming in. You can leave the rest of the values as shown above.

Edit MainActivity

Edit MainActivity to handle the permission requests* the SDK needs to function. The SDK is started in onCreate() and thus will initialize once your app starts.
The example below also shows how you could overwrite the SDK configuration on start-up in onCreate().

🚧

Please note:

As of Android 12 it is not possible anymore to request background locations. The code snippet below is adjusted to this. You should think about showing a rationale to users using Android 12 asking them to enable 'location always' in the app settings.

package com.sdkreactintegration

import android.Manifest
import android.app.Activity
import androidx.annotation.NonNull
import android.app.AlertDialog
import android.content.*
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import com.facebook.react.ReactActivity
import com.motions.sdk.client.logging.LoggerHelper
import com.motions.sdk.client.permissions.PermissionBuilder
import com.motions.sdk.client.permissions.PermissionsCallback
import java.util.logging.Logger
import com.motions.sdk.client.MotionSSDK
import com.motions.sdk.client.configuration.RuntimeConfig


class MainActivity : ReactActivity() {

  var sdk: MotionSSDK? = null

  override fun getMainComponentName(): String? {
    return "SdkReactIntegration" // Replace Project Name
  }


  override fun onCreate(savedInstanceState: Bundle?) {

    // Optional: Load new runtime config to override the values set in MotionS.xml
    // hashMapOf("foo" to "bar", "os" to "android") will set tripMetaData as key-value pairs.
    RuntimeConfig.getInstance(this).createConfig("https://api.motion-s.com", "12dd34", "https://echo.motion-s.com", "https://echo.motion-s.com/activities", true, hashMapOf("foo" to "bar", "os" to "android"), false, "3.4.3")

    super.onCreate(savedInstanceState)
    MotionSDK.createUserRequests(RequestPermission(), PermissionsDenied(), GpsStatus())
    sdk = MotionSDK.start(this)
    }

    companion object {
    private const val PERMISSION_REQUEST_CODE = 34
    
  }

  private fun showLocationPermissionAlert() {
    val alertDialog = AlertDialog.Builder(this).let {
      it.setTitle(R.string.location_permission_title)
      it.setMessage(R.string.location_permission_message)
      it.setPositiveButton(R.string.ok_button) { dialog, _ ->
        dialog.dismiss()
        requestLocationPermission()
      }
      it.create()
    }
    alertDialog.show()
    alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getColor(R.color.viridian_green))
  }


  private fun requestLocationPermission() {

    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
      ActivityCompat.requestPermissions(
              this@MainActivity,
              arrayOf(
                      Manifest.permission.ACCESS_COARSE_LOCATION,
                      Manifest.permission.ACCESS_FINE_LOCATION,
                      Manifest.permission.ACTIVITY_RECOGNITION
              ),
              PERMISSION_REQUEST_CODE
      )
    } else {
      ActivityCompat.requestPermissions(
              this,
              arrayOf(
                      Manifest.permission.ACCESS_COARSE_LOCATION,
                      Manifest.permission.ACCESS_FINE_LOCATION,
                      Manifest.permission.ACTIVITY_RECOGNITION,
                      Manifest.permission.ACCESS_BACKGROUND_LOCATION
              ),
              PERMISSION_REQUEST_CODE
      )
    }
  }


  inner class RequestPermission : PermissionsCallback {
    override fun <T> execute(vararg params: T) {
      val shouldProvideRationaleLocation =
              ActivityCompat.shouldShowRequestPermissionRationale(
                      this@MainActivity,
                      Manifest.permission.ACCESS_FINE_LOCATION
              )

      val shouldProvideRationaleActivity =
              ActivityCompat.shouldShowRequestPermissionRationale(
                      this@MainActivity,
                      Manifest.permission.ACTIVITY_RECOGNITION
              )

      if (shouldProvideRationaleLocation || shouldProvideRationaleActivity) {


        Snackbar.make(
                findViewById(android.R.id.content),
                R.string.permissions_message,
                Snackbar.LENGTH_INDEFINITE
        )
                .setAction(R.string.permissions_action) {
                  ActivityCompat.requestPermissions(
                          this@MainActivity,
                          arrayOf(
                                  Manifest.permission.ACCESS_BACKGROUND_LOCATION,
                                  Manifest.permission.ACCESS_FINE_LOCATION,
                                  Manifest.permission.ACCESS_COARSE_LOCATION,
                                  Manifest.permission.ACTIVITY_RECOGNITION
                          ),
                          PERMISSION_REQUEST_CODE
                  )
                }
                .setActionTextColor(getColor(R.color.viridian_green))
                .show()
      } else {
        showLocationPermissionAlert()
      }
    }
  }

  inner class PermissionsDenied : PermissionsCallback {
    override fun <T> execute(vararg params: T) {
      Log.d("Permission", "Denied")
    }
  }

  inner class GpsStatus : PermissionsCallback {
    override fun <T> execute(vararg params: T) {
      val locationManager =
              ContextCompat.getSystemService(
                      this@MainActivity,
                      LocationManager::class.java
              ) as LocationManager

      val isGpsEnable: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        locationManager.isLocationEnabled
      } else {
        locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
      }

      if (!isGpsEnable) {
        val builder = AlertDialog.Builder(this@MainActivity)
        val action: String = Settings.ACTION_LOCATION_SOURCE_SETTINGS
        builder.setMessage(R.string.gps_status)
                .setPositiveButton(R.string.ok_button) { d: DialogInterface, _: Int ->
                  startActivity(Intent(action))
                  d.dismiss()
                }
        builder.create().show()
      }
    }
  }

}

Create MotionSDK.kt object

Create a new file called MotionSDK.kt in the same directory as your MainActivity.kt and place the following code inside.
This object is used to get an instance of the SDK and start it. It is also possible to append the object to MainActivity instead of having it in a separate file.

import android.app.Activity
import android.util.Log
import com.motions.sdk.client.MotionSSDK
import com.motions.sdk.client.permissions.PermissionBuilder
import com.motions.sdk.client.permissions.PermissionsCallback

object MotionSDK {

    private const val TAG = "MotionSDK"

    private lateinit var permissionRequests: Array<out PermissionsCallback>

    
    fun start(activity: Activity): MotionSSDK? {
        var sdk: MotionSSDK? = null

        try {

            sdk = MotionSSDK.getInstance(activity.applicationContext.packageName)
            sdk?.apply {
                if (!permissionRequests.isNullOrEmpty()) {
                    PermissionBuilder
                        .onRequestPermission(permissionRequests[0])
                        .addCallback()
                        .onPermissionDenied(permissionRequests[1])
                        .addCallback()
                        .onGpsStatus(permissionRequests[2])
                        .build()
                }
                start()
                resume()
            }
        } catch (e: Exception) {

            Log.e(TAG, e.message, e)
        } finally {
            return sdk
        }
    }

    fun createUserRequests(vararg requests: PermissionsCallback) {
        permissionRequests = requests
    }
}

Create SDKManager.kt and SDKPackage.kt

Create SDKPackage class so that the SDKManager you are going to create next can be use on the React side of your app.

SDKPackage:

package com.sdkreactintegration

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import java.util.*
import com.motions.sdk.client.MotionSSDK

class SDKPackage : ReactPackage {
    override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
        val modules = ArrayList<NativeModule>()
        modules.add(SDKManager(reactContext))
        return modules
    }

    override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
        return Collections.emptyList<ViewManager<*, *>>()
    }

}

Create the SDKManager class with the functions you can call from javascript after importing:

package com.sdkreactintegration

import com.sdkreactintegration.MainActivity
import android.content.Intent
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.motions.sdk.client.MotionSSDK
import android.util.Log
import com.facebook.react.bridge.Callback
import com.motions.sdk.client.configuration.RuntimeConfig


class SDKManager(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

    override fun getName(): String {
        return "SDKManager"
    }

    var sdk: MotionSSDK? = null

    @ReactMethod
    fun configureAndroid(baseUrl: String,
                         apiKey: String,
                         url: String,
                         activityUrl: String,
                         customsettings: Boolean,
                         tripMetaData: HashMap<String, String>,
                         activites: Boolean,
                         version: String) {

        RuntimeConfig.getInstance(reactApplicationContext.getApplicationContext()).createConfig(
                baseUrl,
                apiKey,
                url,
                activityUrl,
                customsettings,
                tripMetaData,
                activites,
                version
        )
    }

    @ReactMethod
    fun start() {
        sdk = MotionSSDK.getInstance(reactApplicationContext.getApplicationContext().packageName)
        sdk?.start()
    }

    @ReactMethod
    fun stop() {
        sdk = MotionSSDK.getInstance(reactApplicationContext.getApplicationContext().packageName)
        sdk?.stop()
    }



    @ReactMethod
    fun setDriverId(id: Long) {
        MotionSSDK.updateDriverId(id)
    }


    @ReactMethod
    fun setDeviceId(id: String) {
        MotionSSDK.updateDeviceId(id)
    }

    @ReactMethod
    fun getDriverId(callback: Callback) {
        val driverId = MotionSSDK.getDriver()?.id.toString()
        callback.invoke(driverId)
    }

}

The SDK is started automatically and will start recording right away. It is possible to stop the recording by calling the stop() function. Calling start() will then start the recording again.

You can set a new RuntimeConfiguration programatically by calling the configureAndroid() method. To set driverId and deviceId you can call setDriverId() and setDeviceId() respectively.

1.2 iOS

Adding the iOS SDK

Drop the .xcframework into your framework folder and import with default settings.

Add code to AppDelegate.swift

Replace the content of the default AppDelegate.swift with the following content:

import UIKit
import MotionSDK
import CoreMotion
import Combine

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
    func sourceURL(for bridge: RCTBridge!) -> URL! {
        #if DEBUG
            return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
        #else
            return Bundle.main.url(forResource:"main", withExtension:"jsbundle")
        #endif
}

var window: UIWindow?
var bridge: RCTBridge!
var tripRecorder: AutomaticRecorder?
  

@objc static func requiresMainQueueSetup() -> Bool {
       return false
  }

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    self.bridge = RCTBridge(delegate: self, launchOptions: launchOptions)

    let rootView = RCTRootView(bridge: self.bridge, moduleName: "SdkReactIntegration", initialProperties: nil)

    self.window = UIWindow(frame: UIScreen.main.bounds)
    let rootViewController = UIViewController()

    rootViewController.view = rootView

    tripRecorder = AutomaticRecorder()

    tripRecorder?.start()

  // Optional to load new RuntimeConfig on startup --- >
  #if targetEnvironment(simulator)
  let config = Config(
      uploadURL: "http://localhost/accepts-json-post-bodies",
      uploadHeader: ["Authorization": "Basic 12345"],
      tripMetadata: ["fromSimulator": "true"]
      )
  #else
      let config = Config(
      uploadURL: "https://echo.motion-s.com",
      uploadHeader: ["Authorization": "Basic 12345"],
      tripMetadata: ["Device": "XYZ"]
      )
  #endif

  RuntimeConfig.current.updateConfig(from: config)
  // < --- Optional
  
  
    self.window!.rootViewController = rootViewController;
    self.window!.makeKeyAndVisible()
    return true
}
  
}

You need to add the following resources to your info.plist file for the location permissions to work. You can right-click

Add values to .plist file

    <key>NSBluetoothAlwaysUsageDescription</key>
	<string>Need BLE permission</string>
	<key>NSBluetoothPeripheralUsageDescription</key>
	<string>Need BLE permission</string>
	<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
	<string>Description</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>Will you allow this app to always know your location?</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>Do you allow this app to know your current location?</string>
	<key>NSMotionUsageDescription</key>
	<string>We need GPS to record trips</string>
	<key>UIBackgroundModes</key>
	<array>
		<string>location</string>
	</array>

And of course, do not forget to enable location updates in background modes under the Signing & Capabilities menu of your target.

Create SDKManager.swift class

Create a swift class called SDKManager to reflect the implementation on android. This class will hold all the functions you wish to call from the javascript side of your app. Like with Android, the iOS SDK has a start() and stop() function to start or stop the recording. Similar to android but with fewer parameters you can configure the SDK during runtime with the configure() function. sync() forces the upload of pending trips, although this should be handled automatically by the SDK.
Place the following content into the SDKManager.swift

//
//  SDKManager.swift
//  SdkReactIntegration
//
//  Created by motion-s on 03/01/2022.
//

import Foundation
import MotionSDK
import React



@available(iOS 13.0, *)
@objc(SDKManager)
class SDKManager: NSObject {
  
  var tripRecorder: AutomaticRecorder?
  
  @objc static func requiresMainQueueSetup() -> Bool {
       return false
   }

  
  @objc func getName(_ callback: RCTResponseSenderBlock) {
  callback(["SDKManager"])
  }
  
  @objc func start() -> Void {
    if tripRecorder == nil {
               tripRecorder = AutomaticRecorder()
        }
           tripRecorder?.start()
    }
  
  
  @objc func stop() -> Void {
    tripRecorder?.stop()
  }
  
  
  @objc func sync() -> Void {
    DispatchQueue.global(qos: .background).async {
               do {
                   try APIBackend.shared.uploadPending()
                 
               } catch {
            }
       }
  }
  
  
  
  @objc func configure(_ url: NSString?, header: NSDictionary, metaData: NSDictionary) -> Void {
    
    let url = RCTConvert.nsString(url)
    
    let config = Config(
      uploadURL: url,
      uploadHeader: header as? Dictionary<String, String>,
      tripMetadata: metaData as? Dictionary<String, String>
    )
    RuntimeConfig.current.updateConfig(from: config)
  }
 }

Create RTCBridge.m file next to your swift classes

//
//  RTCBridge.m
//  SdkReactIntegration
//
//  Created by motion-s on 03/01/2022.
//

#import <Foundation/Foundation.h>

#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(SDKManager, NSObject)
RCT_EXTERN_METHOD(start)
RCT_EXTERN_METHOD(stop)
RCT_EXTERN_METHOD(sync)
RCT_EXTERN_METHOD(configure:(NSString *)url header:(NSDictionary *)header metaData:(NSDictionary *)metaData)
@end

Add RCTBridgeModule and RCTBridge to your Bridging-Header

The file should look like this:

#import “React/RCTBridgeModule.h”
#import “React/RCTBridge.h”
#import “React/RCTEventDispatcher.h”
#import “React/RCTRootView.h”
#import “React/RCTUtils.h”
#import “React/RCTConvert.h”
#import “React/RCTBundleURLProvider.h”
//
// Use this file to import your target’s public headers that you would like to expose to Swift.
//

Interfacing the SDKs on the React side

As both the Android and the iOS SDK act like black boxes which handle recording and uploading of the data by themselves, there is very little configuration to be done on your side. One thing that has to be handled is the creation of a driver and optionally also a device. How you decide to create a driver is up to you, but you should configure the SDKs with a unique identifier like a driver_id.
As shown in the previous sections, both the Android and the iOS SDKs can be configured during runtime and the driver_id should be provided in the trip metadata.

After importing the SDKManager configure the SDKs like so (example taken from App.js in the sample app):

import SDKManager from './SDKManager';


configureRC = () => {

     if (Platform.OS === 'android') {

                          SDKManager.configureAndroid("https://api.example.com/",  // baseUrl -> not used in your case
                                                    "1234", // apiKey 
                                                    "https://api.example.com/upload", // url 
                                                    "https://api.example.com/activities",  // activityUrl -> not used
                                                    true,  // customSettings -> set to true
                                                    "foo=bar,key=value", // tripMetadata -> add key/value pairs
                                                    false, // -> leave at false
                                                    "1.0.0");}

               else if (Platform.OS === 'ios') {
                          SDKManager.configure("https://api.example.com/upload", // url
                                            {"x-api-key":"12345"}, // apiKey
                                            {"foo":"bar"}); // tripMetadata

               }
               else {
                    console.log("unsupported platform");
               }
        }