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.
Updated over 2 years ago