Version 0.4.2

  • Current version: 0.4.2
  • Previous versions
  • Main features
    • Local cache data
    • Support multiple sampling intervals for locations polling
    • Support multiple syncing intervals to the cloud
    • Provide detailed information on function success
    • Handles multiple engine types for motorized transportation

Overview

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)

Installing the MOPRIM TMD Sdk

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.

Add the MOPRIM TMD Sdk dependencies

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.2"
    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.7.1"
    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.43.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:21.0.0'
	// Optional dependencies for collecting TMD background events
	implementation 'org.greenrobot:eventbus:3.3.1'
}

Manifest

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_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

Reasons for the aforementied permissions:

  • FOREGROUND: The MOPRIM TMD Service requires to be always on to monitor your mobility. Background service executions are limited to a few minutes.
  • ACCESS_FINE_LOCATION: The MOPRIM TMD Service requires to know your precise location to build your mobility path.
  • ACCESS_COARSE_LOCATION: The fine location permission requires the coarse location permission.
  • ACCESS_BACKGROUND_LOCATION: The MOPRIM TMD Service requires to know your location in the background, even when the app is closed or not in use.
  • RECEIVE_BOOT_COMPLETED: The MOPRIM TMD Service to restart automatically on reboot.

Location 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.

Using the MOPRIM TMD Sdk

Overview

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. Recommended app architecture while using MOPRIM TMD SDK

Initialization

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

        // If you want to be able to start TMD from background after successful configuration task if TMD start was previously called
        TMD.getInstance().setStartCallbackWorkerClass(getApplicationContext(), TmdBackgroundStartJob.class);
		
        // Initialize the TMD (it may throw IllegalStateException if the configuration is)
        TMD.getInstance().init(this, builder.build());
    }
}

with


import android.content.Context;

import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

import fi.moprim.tmd.sdk.TMD;
import fi.moprim.tmd.sdk.model.TmdError;
import fi.moprim.tmd.sdk.model.TmdException;

public class TmdBackgroundStartJob extends Worker {

    public TmdBackgroundStartJob(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
        if (TMD.getInstance().isInitialized()) {
            startTMD(getApplicationContext());
            return Result.success();
        }
        else {
            return Result.retry();
        }
    }
}

Starting the TMD service

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.

Example of a setup to start the TMD in foreground

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 MainActivity
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
	    PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
			new Intent(context, MobilityMeterActivity.class), PendingIntent.FLAG_IMMUTABLE);
        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());
	}
}

Stopping the TMD service

Alternatively, you can stop the TMD with:

TMD.stop(context);

TMD lifecycle

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:

Add broadcast receiver to handle the restart of the service

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);
        }
    }

}

Add the receiver to your application in the 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 data

MOPRIM TMD SDK collects a variety of data:

  • TmdActivity: a TmdActivity object contains information about the recognized mean of transport (e.g., non-motorized/pedestrian/walk, stationary or motorized/road/car) of the mobile phone carrier between an interval of time. It exposes the following information:
    • 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
    • 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 metadata attached to this activity
    • The vehicle profile attached to this activity
  • TmdStats: A daily statistical summary of user’s mobility. It exposes the following information:
    • The date (in milliseconds)
    • The mode of transport
    • The timestamp when the mode was last updated to the local database
    • The timestamp when the mode was last modified in the MOPRIM Cloud
    • The number of legs (i.e., the number of TmdActivity) during that day
    • The accumulated duration of the mode for that day (in seconds)
    • The accumulated distance of the mode for that day (in meters)
    • The accumated C02 value (in grams) for that day
    • A flag to tells if the stats are for the user or the aggregated stats for the user’s of the whole service (to build comparison between the user and the rest of the users).
    • The number of daily active users to generate the stats for that day (will always be one for the user’s stats and at least 1 for the aggregated users’ stats).

In case, you have added the MOPRIM TMD SDK Trips data plugin, you also get access to:

  • Trip: A representation of consecutive TmdActivity objects which belong to the same journey. It exposes the following information:
    • The timestamp (in milliseconds) when the trip started
    • The timestamp (in milliseconds) when the trip stopped
    • The label for the main mode of the trip (longest cummulated duration)
    • The accumulated C02 (in grams) for the trip
    • The accumulated distance (in meters) covered during the trip
    • The duration (in seconds) of the trips
    • The legs of the trips (i.e., ordered TmdActivity objects)
    • The metadata attached to this trip
    • A flag telling if the trip has been completed (a trip is considered completed is the user’s has been stationary for a consequent period of time)

The MOPRIM TMD SDK includes an utility to encode or decode polylines.

Repositories and RefreshLiveData

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 singleton which can be accessed via 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.
  • TmdActivityRepository singleton which can be accessed via 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 singleton which can be accessed via 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.

Example of a ViewModel exposing the last trip

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);}
}

MOPRIM TMD low-level Cloud API

The MOPRIM TMD Sdk exposes low-level functions for the developer to directly communicate with the Cloud

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

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.

Fetching data from the MOPRIM Cloud

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.

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 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.

Annotating an activity (i.e., add metadata or vehicle profile)

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, vehicleProfile);
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.

Fetching statistics about the user mobility

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.

Forcing the upload of TMD data to our cloud

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.

Reacting to TMD background operations

As the TMD periodically uploads/downloads the data to the cloud, we enabled to possibility for application developers to react to these uploads using the EventBus events from the Android SDK.

Registering a pending intent with:

// If you want to do trigger some process when the Moprim produces events, you can make use of the EventBus library.
@Override
public void onCreate() {
    super.onCreate();
	EventBus.getDefault().register(this);
}

@Override
public void onDestroy() {
    super.onCreate();
	EventBus.getDefault().unregister(this);
}

@Subscribe(threadMode = ThreadMode.MAIN)
public void onTmdEvent(TmdBackgroundEvent event) {
    if (event != null) {
        Log.i(TAG, event.toString());
    }
}

TmdBackgroundEvent is a simple object describing the TMD background event, including the type of operation that was performed. The list of background operations is available here.

Possible errors return by the MOPRIM TMD Sdk functions

The list of possible errors returned by any TMD function is available in the javadoc.

Changelog

  • Version 0.4.2
    • Remove dependency to deprecated mutable PendingIntent, and replace it with optional dependency to EventBus
    • Support attaching a vehicle profile (e.g., engine type) to discovered modes
  • Version 0.4.0
    • Uses Androidx framework
    • Provides repositories to download/upload data from/to the MOPRIM Cloud
    • Moprim data as LiveData
  • Version 0.3.5
    • Bug fixes
    • Default location update set to 10s
    • Default data upload interval is set to 15m
  • Version 0.3.4
    • Initial release of the Android SDK

Contact

Contact us for getting a quote for the SDK.