Native Android

ℹ️Covered in this guide
Requirements for creating an Android App
Installing the Motion-S SDK
Using the Motion-S SDK
Example Setup
Fetching Data from the Motion-S backend
Using your own backend

Adding the android .aar library to your 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.

In the top-level build.gradle add the following lines:

allprojects {
    repositories {

        flatDir {
            dirs project(':app').file('libs')
        }
    }
}

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

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:

    kotlin_version = '1.5.10'
 
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.0'
    implementation 'com.google.android.material:material:1.4.0'

    implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
    implementation("io.reactivex.rxjava2:rxkotlin:2.3.0")
    implementation("com.google.firebase:firebase-analytics:20.0.0")
    implementation("com.google.firebase:firebase-analytics-ktx:20.0.0")
    implementation("com.google.android.gms:play-services-location:+")
    implementation("com.google.android.gms:play-services-location:18.0.0")
    implementation("com.google.code.gson:gson:2.8.6")
    implementation("androidx.work:work-runtime:2.7.1")
    implementation("androidx.work:work-rxjava2:2.7.1")
    implementation("androidx.security:security-crypto:1.1.0-alpha03")
    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.8.0")
    implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
    implementation("androidx.room:room-runtime:2.3.0")
    implementation("androidx.room:room-ktx:2.3.0")
    implementation("com.jakewharton.threetenabp:threetenabp:1.2.3")
    implementation("org.conscrypt:conscrypt-android:2.2.1")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20")
    implementation("androidx.room:room-rxjava2:2.3.0")
  

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" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

Add String values and Motion-S.xml

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

<resources>
   <string name="app_name"></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 by setting 'customSettings' to true and and 'url' to some endpoint of yours. 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.motions.whitelabel.activities

import android.Manifest
import android.app.AlertDialog
import android.content.*

import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import com.google.android.material.snackbar.Snackbar
import com.motions.sdk.client.MotionSSDK
import com.motions.sdk.client.logging.LoggerHelper
import com.motions.sdk.client.permissions.PermissionsCallback

import com.motions.whitelabel.R
import com.motions.whitelabel.sdk.MotionSDK
import com.motions.whitelabel.ui.Presenter
import dagger.hilt.android.AndroidEntryPoint
import java.util.logging.Logger
import javax.inject.Inject


@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

   private val log: Logger = LoggerHelper.getLogger(MainActivity::class.java)

   private lateinit var navController: NavController
   private lateinit var appBarConfiguration: AppBarConfiguration

   @Inject
   lateinit var presenter: Presenter

   var sdk: MotionSSDK? = null

   companion object {
       private const val PERMISSION_REQUEST_CODE = 34

   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setTheme(R.style.AppTheme)
       setContentView(R.layout.activity_main)

       navController = Navigation.findNavController(this, R.id.nav_host_fragment)
       appBarConfiguration = AppBarConfiguration.Builder(navController.graph).build()
       presenter.activity = this
       MotionSDK.createUserRequests(RequestPermission(), PermissionsDenied(), GpsStatus())

       sdk = MotionSDK.start(this)
   }



   override fun onSupportNavigateUp(): Boolean {
       val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
       return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp()
   }

   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) {
               log.info("Displaying permission rationale to provide additional context.")

               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_COARSE_LOCATION,
                               Manifest.permission.ACCESS_FINE_LOCATION,
                               Manifest.permission.ACCESS_BACKGROUND_LOCATION,
                               Manifest.permission.ACTIVITY_RECOGNITION,

                           ),
                           PERMISSION_REQUEST_CODE
                       )

                   }
                   .setActionTextColor(getColor(R.color.viridian_green))
                   .show()
           } else {
               log.info("Requesting permission")


               showLocationPermissionAlert()
           }
       }
   }

   inner class SettingsListener : View.OnClickListener {

       override fun onClick(v: View) {
           Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:${callingActivity!!.packageName}")).apply {
               addCategory(Intent.CATEGORY_DEFAULT)
               addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
               startActivity(this)
           }
       }
   }

   inner class PermissionsDenied : PermissionsCallback {
       override fun <T> execute(vararg params: T) {
           Snackbar.make(
               findViewById(android.R.id.content),
               R.string.permissions_denied,
               Snackbar.LENGTH_INDEFINITE
           )
       }
   }

   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

           isGpsEnable = 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's 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
   }
}

Fetching Data from the Motion-S backend

To get the enriched data from our backend can import the MotionSDataManager class from the SDK. It provides the following methods:

fun fetchTrips(startDate: ZonedDateTime, endDate: ZonedDateTime, offset: Int, limit: Int): Single<MutableList<Trip>>

fun fetchRemoteTrips(startDate: String?, endDate: String?): Single<MutableList<Trip>>

fun fetchTripDetail(trip: Trip): Single<TripDetail>

fun fetchTripDetail(id: Long): Single<TripDetail>

fun fetchTripProfile(timezone: String): Single<MutableList<Profile>>

fun fetchRemoteTripProfile(timezone: String): Single<MutableList<Profile>>

fun fetchTrip(tripUid: String): Single<Trip>

fun fetchLastTrip(): Single<SummaryDates?>

fun fetchFirstTrip(): Single<SummaryDates?>

fun updateDriver(name: String, email: String, phone: String): Single<Driver>

Example usage

fun fetchRemoteTrips() {
       subscriptions.add(
           MotionSDataManager.fetchRemoteTrips(null, lastTripStartDate)
               .subscribeOn(Schedulers.io())
               .subscribe({
                   if (it.isNotEmpty()) {
                       lastTripStartDate = it.last().summary?.startTime
                   }
                   state.postValue(State.SUCCESS)
               }, {
                   state.postValue(State.ERROR)
                   log.severe(it.message)
               })
       )
   }

Alternatively, you can query data directly from our API endpoints. This will keep the communication between the SDK and your app at a bare minimum.

Runtime Configuration

import com.motions.sdk.client.configuration.RuntimeConfig
fun configureAndroid(baseUrl: String,
                        apiKey: String,
                        url: String,
                        activityUrl: String,
                        customsettings: Boolean,
                        tripMetaData: HashMap<String, String>,
                        activites: Boolean,
                        version: String) {

       RuntimeConfig.getInstance(context).createConfig(
               baseUrl,
               apiKey,
               url,
               activityUrl,
               customsettings,
               metaDataMap as HashMap<String, String>,
               activites,
               version
       )
   }

Using your own backend

To record trips and send them to your backend, set customSettings to true and specify your upload URL in the URL field. The apiKey is sent as the header with every request and can serve to secure your endpoint. If you use the SDK to send trips to your custom endpoint, you do not need to modify any other parameter. Leave them unchanged.