0.4.0
The MOPRIM TMD Sdk for Android when install and properly configured will analyze data streams for the mobile device’s motion sensors to capture how its owner moves. Once the Moprim TMD service is started, the SDK automatically process the data and synchronize its results with our cloud. The final outputs of the TMD will be served through our cloud.
Pre-requisite: the MOPRIM TMD Sdk works on all versions of the Android Sdk >= 19 (Android 4.4, KitKat)
Before you start to use the MOPRIM TMD Sdk in your Android application, you will need to add the SDK as a dependency. The MOPRIM Sdk is already minimized and obfuscated to limit its size but it requires some runtime dependency to function. If you do not have the MOPRIM TMD Sdk, you can contact us to get a quote and developer API key.
You need to copy the MOPRIM TMD Sdk files, such as tmd-sdk-core-0.4.0.arr
or tmd-sdk-trisp-0.4.0.arr
to the libs
folder in the root folder of your application.
Then you must do these changes to your build.gradle
file:
dependencies {
// TMD SDK dependency
// Please make sure that all Moprim SDK module are using the same version
def tmd_version = "0.4.0"
implementation files("libs/tmd-sdk-core-${tmd_version}.aar")
implementation files("libs/tmd-sdk-trips-${tmd_version}.aar") // Optional to enable the Trip data plugin
// SDK minimal dependencies
runtimeOnly "androidx.work:work-runtime:2.5.0"
runtimeOnly group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'
runtimeOnly group: 'com.mkobos', name: 'pca_transform', version: '1.0.2'
// The SDK uses AWS Amplify internally and was compiled with the following version
def aws_version = "2.21.0"
runtimeOnly group: 'com.amazonaws', name: 'aws-android-sdk-core', version: "$aws_version"
runtimeOnly group: 'com.amazonaws', name: 'aws-android-sdk-apigateway-core', version: "$aws_version"
// Location dependency
runtimeOnly 'com.google.android.gms:play-services-location:18.0.0'
}
To successfully use the MOPRIM TMD Sdk, you need to make sure the following permissions are added to your Manifest.xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
Reasons for the aforementied permissions:
The MOPRIM TMD Sdk requires the access to the user’s precise location even when the app is in the background or not in use. The location permissions must be granted before the TMD is started.
We recommend to add the dependency to EasyPermissions to handle the permissions granting process.
Following is an example to grant the location permissions in an Activity or Fragment, as required by MOPRIM TMD:
private static final int TMD_PERMISSIONS_REQUEST_LOCATION = 1212; // Unique permission ID
private void checkPermissions() {
String[] perms;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
perms = new String[]{ Manifest.permission.ACCESS_BACKGROUND_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};
}
else {
perms = new String[]{ Manifest.permission.ACCESS_FINE_LOCATION};
}
if (!EasyPermissions.hasPermissions(this, perms)) {
// Do not have permissions, request them now
TMD.stop(getApplicationContext()); // Stop the TMD
EasyPermissions.requestPermissions(this, "We need to access location", TMD_PERMISSIONS_REQUEST_LOCATION, perms);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// Forward results to EasyPermissions
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
@Override
protected void onResume() {
super.onResume();
checkPermissions();
}
The MOPRIM SDK takes advantage of the AndroidX framework component and exposes observable objects (LiveData) to the developer.
Compared to versions 0.3.x
, this version of the SDK greatly minimizes the work of integration by the developer by handling all communications with the Moprim Cloud in the background.
The SDK requires an API key and an API enpoint configuration. Configuration of the TMD is best done in the Application class. If you don’t already have an Application class, then create one. Apply the configuration to your Application class.
import android.app.Application;
import android.util.Log;
import fi.moprim.tmd.sdk.TMD;
import fi.moprim.tmd.sdk.TmdConfig;
// If you are using MOPRIM plugins, they must be added to the configuration here
import fi.moprim.tmd.sdk.trips.plugin.TripPlugin;
public class HelloWorldApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
TmdConfig.Builder builder = new TmdConfig.Builder(this)
// Mandatory
.setEndpoint(getString(R.string.tmd_sdk_config_endpoint))
.setKey(getString(R.string.tmd_sdk_config_key))
// Optional config parameters
.setUploadIntervalMinutes(15) // fastest interval
.setDefaultMobileDataAllowed(true) // if using mobile data for background operation if enabled by default
.addDataPlugin(new TripPlugin.Builder().build()); // If you are using the TripPlugin
// Initialize the TMD (it may throw IllegalStateException if the configuration is)
TMD.getInstance().init(this, builder.build());
}
}
The TMD service is currently only available in foreground, and should be called by:
TMD.startForeground(context, NOTIFICATION_ID, notification);
where the NOTIFICATION_ID and the notification are the communication medium for foreground application.
static final String NOTIFICATION_CHANNEL_ID = "your.app.tmd.channel";
static final int NOTIFICATION_ID = 7777; // Unique notification id
public static Notification buildNotification(@NonNull Context context) {
// Create notification builder.
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
notificationBuilder.setWhen(System.currentTimeMillis());
notificationBuilder.setSmallIcon(android.R.drawable.ic_media_play);
notificationBuilder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher));
notificationBuilder.setContentTitle("TMD demo");
notificationBuilder.setContentText("TMD is running");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
notificationBuilder.setPriority(NotificationManager.IMPORTANCE_HIGH);
}
// Make the notification open the app
PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
new Intent(context, YourMainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
notificationBuilder.setContentIntent(contentIntent);
// Build the notification.
return notificationBuilder.build();
}
public static void createNotificationChannel(@NonNull Context context) {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "TMD channel name";
String description = "TMD channel description";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel =
new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
channel.setSound(null, null);
channel.setVibrationPattern(null);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
NotificationChannel notificationChannel =
notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
if (notificationChannel == null) {
notificationManager.createNotificationChannel(channel);
}
}
}
}
public static void startTMD(@NonNull Context context) {
createNotificationChannel(context);
Notification notification = buildNotification(context);
TmdStartReturnCode returnCode = TMD.startForeground(context, NOTIFICATION_ID, notification);
if (returnCode != TmdStartReturnCode.STARTED && returnCode != TmdStartReturnCode.ALREADY_RUNNING) {
Log.e(TAG, "Start TMD failed because of this reason: " + returnCode.name());
}
}
Alternatively, you can stop the TMD with:
TMD.stop(context);
Applications using the TMD may be killed upon reboot or application updates. In order to keep the TMD service running, one must do the following:
public final class MyBroadcastReceiver extends BroadcastReceiver {
/**
* Checks if the action provided by the intent is relevant to boot complete
* or a app update
*
* @param action the string action
* @return true if the action if relevant to boot complete or an app update
*/
private boolean checkActionIsValid(String action) {
return "android.intent.action.BOOT_COMPLETED".equals(action) ||
"android.intent.action.QUICKBOOT_POWERON".equals(action) ||
"android.intent.action.MY_PACKAGE_REPLACED".equals(action);
}
/**
* Called when intent are being received by the receiver
*
* @param context the context
* @param intent the intent
*/
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && checkActionIsValid(intent.getAction()) {
TMD.startForeground(context, NOTIFICATION_ID, notification);
}
}
}
AndroidManifest.xml
file<application>
<receiver android:name=".MyBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
Also make sure that the following permission is in your Manifest.xml
:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
MOPRIM TMD SDK collects a variety of data:
non-motorized/pedestrian/walk
, stationary
or motorized/road/car
) of the mobile phone carrier between an interval of time. It exposes the following information:
In case, you have added the MOPRIM TMD SDK Trips data plugin, you also get access to:
The MOPRIM TMD SDK includes an utility to encode or decode polylines.
MOPRIM TMD Sdk exposes a number of object to simplify the integration of the library by the developer. After the library has been initialized, a number of repositories can be accessed:
TmdActivityRepository.getInstance()
. It provides the following methods:
getTmdActivities(@NonNull Context context, long start, long end)
which returns a RefreshLiveData<Resource<List<TmdActivity>>>
observable object for the list of TmdActivity object between two timestamps.getTmdActivities(@NonNull Context context, int last)
which returns a RefreshLiveData<Resource<List<TmdActivity>>>
observable object for the list of TmdActivity object for the last last
days, current day included.getLastTmdActivity(@NonNull Context context)
which returns a RefreshLiveData<Resource<TmdActivity>>
observable object for the most recent TmdActivity detected by the SDK.TmdStatsRepository.getInstance()
. It provides the following methods:
getTmdStats(@NonNull Context context, int last)
which returns a RefreshLiveData<Resource<List<TmdStats>>>
observable object for the list of TmdStats object for the last last
days, current day included.In case, you have added the MOPRIM TMD SDK Trips data plugin, you also get access to the trip repository:
TripRepository.getInstance()
. It provides the following methods:
getTrips(@NonNull Context context, long start, long end)
which returns a RefreshLiveData<Resource<List<Trip>>>
observable object for the list of Trip object between two timestamps.getLastTrips(@NonNull Context context, int last)
which returns a RefreshLiveData<Resource<List<Trip>>>
observable object for the list of the last
Trip objects.getLastTrip(@NonNull Context context)
which returns a RefreshLiveData<Resource<Trip>>
observable object of the last Trip object.All repositories return RefreshLiveData, an extension of LiveData which permits to reload the data either from the local database or the remote Moprim Cloud. The data observed is a Resource object which gives information about the status of the request (e.g., LOADING, SUCCESS or ERROR). It allows to quickly modify the state of the UI by listening to these statuses.
An advantage of the repository is that network calls are effectuated in background threads automatically.
The following example is a view model that transforms the last Trip to a custom String representation. It provides a method to get the LiveData for the representation and two methods to refresh the LiveData.
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import fi.moprim.tmd.sdk.data.RefreshLiveData;
import fi.moprim.tmd.sdk.data.RefreshMode;
import fi.moprim.tmd.sdk.data.Resource;
import fi.moprim.tmd.sdk.model.TmdActivity;
import fi.moprim.tmd.sdk.trips.data.TripRepository;
import fi.moprim.tmd.sdk.trips.model.Trip;
import fi.moprim.tmd.sdk.utilities.StringUtils;
public class YourViewModel extends ViewModel {
private final RefreshLiveData<Resource<Trip>> refreshLiveData;
private final LiveData<Resource<String>> representationLiveData;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH);
public YourViewModel(@NonNull Context context, @NonNull TripRepository repository) {
this.refreshLiveData = repository.getLastTrip(context);
this.representationLiveData = Transformations.map(this.refreshLiveData, input -> {
if (input == null) {
return null;
}
else if (input.data == null) {
return Resource.copyInto(input, null);
}
else {
return Resource.copyInto(input, String.format(Locale.ENGLISH, "%s (%s)\n[%d]: %s",
DATE_FORMAT.format(new Date(input.data.getTimestampStart())),
StringUtils.getDuration(input.data.getDuration()),
input.data.size(), formatLegs(input.data)));
}
});
}
@NonNull
private static String formatLegs(@NonNull Trip trip) {
if (trip.size() == 0) return "";
StringBuilder sb = new StringBuilder();
for (TmdActivity leg : trip.getLegs()) {
if ("stationary".equals(leg.getActivity())) {
sb.append('.');
} else {
String[] subActivities = leg.getActivity().split("/");
if (subActivities.length == 0 || subActivities[subActivities.length - 1].length() == 0) sb.append("?");
else sb.append(subActivities[subActivities.length - 1].substring(0, 1).toUpperCase());
}
}
return sb.toString();
}
public LiveData<Resource<String>> getRepresentationLiveData() {
return representationLiveData;
}
public void refresh(@NonNull RefreshMode refreshMode) {
this.refreshLiveData.refresh(refreshMode);
}
public void refresh() { refresh(RefreshMode.DEFAULT);}
}
The MOPRIM TMD Sdk exposes low-level functions for the developer to directly communicate with the Cloud
TMD metadata contains information about the information that our cloud knows about a user:
More information about TmdCloudMetadata can be found in the javadoc.
In order to get the TMD Cloud metadata, please run the TmdCloudApi#fetchMetadata in a background thread.
Result<TmdCloudMetadata> result = TmdCloudApi.fetchMetadata(this);
if (result.hasMessage()) {
Log.i(TAG, result.getMessage());
}
if (result.hasResult()) {
Log.d(TAG, result.getResult());
}
if (result.hasError()) {
Log.e(TAG, result.getError());
}
This function requires a network call, thus cannot be execute on the main thread. It also requires the TMD to be initialized beforehand.
TMD Activities
The unique id of the activity when stored in the local cache
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 javadoc.
In order to get the list of TMD activities, please run the TmdCloudApi#fetchData or alike in a background thread.
Result<List<TmdActivity>> result = TmdCloudApi.fetchData(this, date);
if (result.hasMessage()) {
Log.i(TAG, result.getMessage());
}
if (result.hasResult()) {
Log.d(TAG, Arrays.toString(result.getResult()));
}
if (result.hasError()) {
Log.e(TAG, result.getError());
}
This function requires a network call, thus cannot be execute on the main thread. It also requires the TMD to be initialized beforehand.
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 function.
Example:
Result<TmdActivity> result = TmdCloudApi.correctActivity(context, tmdActivity, newLabel);
if (result.hasMessage()) {
Log.i(TAG, result.getMessage());
}
if (result.hasResult()) {
Log.d(TAG, result.getResult());
}
if (result.hasError()) {
Log.e(TAG, result.getError());
}
The Android 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.
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 function.
Example:
Result<TmdActivity> result = TmdCloudApi.annotateActivity(context, tmdActivity, myMetadata);
if (result.hasMessage()) {
Log.i(TAG, result.getMessage());
}
if (result.hasResult()) {
Log.d(TAG, result.getResult());
}
if (result.hasError()) {
Log.e(TAG, result.getError());
}
It is possible to update the label and add metadata at the same time by using the TmdCloudApi#updateActivity function.
TMD statistics contains information about a list of statistics for each type of activity and during 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.
Check the javadoc for more information.
Result<TmdStats> result = TmdCloudApi.fetchStats(context, nbOfDays);
if (result.hasMessage()) {
Log.i(TAG, result.getMessage());
}
if (result.hasResult()) {
Log.d(TAG, result.getResult());
}
if (result.hasError()) {
Log.e(TAG, result.getError());
}
For more information about TmdStats, check out the javadoc.
This function requires a network call, thus cannot be execute on the main thread. It also requires the TMD to be initialized beforehand.
Our system usually upload itself the data to our cloud periodically (default interval is every hour), but it is possible for the application developer to force upload the data to our cloud.
The current modality is never synchronized with the cloud as it is not considered as completed.
To force upload the data to our cloud, use the following method:
Result<TmdUploadMetadata> result = TmdCloudApi.uploadData(context);
if (result.hasMessage()) {
Log.i(TAG, result.getMessage());
}
if (result.hasResult()) {
Log.d(TAG, result.getResult());
}
if (result.hasError()) {
Log.e(TAG, result.getError());
}
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 javadoc.
This function requires a network call, thus cannot be execute on the main thread. It also requires the TMD to be initialized beforehand.
As the TMD periodically uploads/downloads the data to the cloud, we enabled to possibility for application developers to react to these uploads using PendingIntent from the Android SDK.
Registering a pending intent with:
// If you want to do trigger some process when the Moprim produces events
Intent intent = new Intent(MyApplication.this, MyIntentService.class);
PendingIntent callbackIntent = PendingIntent.getService(MyApplication.this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
TmdCloudApi.setCallbackIntent(callbackIntent);
You can check out the javadoc. The list of background operations is available here.
The list of possible errors returned by any TMD function is available in the javadoc.
Contact us for getting a quote for the SDK.