Create a virtual sensor extension for IoTool

Virtual sensor extension is a extension that gets data from a local database made by IoTool application on your device. It is meant to execute logical operations on recently collected data. This document provides information on what you have to do to create your own custom virtual sensor and demo source code.

General things

Before going further in to the document, it is important to note that this document only extends on information provided by document found on this link.

Reading from database

IoTool application and its library implements simple way of reading from database. To read data from database, use IoToolDataReader class. IoTool application uses multiple databases. Each database is assign to their sensor using service ID and it contains a single table called "data". Out of all columns in data table there are four important columns:

  • timestamp: Indicates when the values where writen (String representation of Long value)

  • vals: Text containing values separated by simbol ";" (Contains String representation of Double values)

  • times: Text containing timestamps separated by simbol ";" (Contains String representation of Long values)

  • type: Text containing type value

Querying from database is similar to the SQLiteDataBase class, difference being that IoToolDataReader has a built in cursor and that we get time or value from calling getTime() or getValue() method.

constructor IoToolReading(String readingID, File appDir):

  • readingID: String that has a format "<reading>@<serviceID>" (In this example: "Reading1@DemoVS")

  • appDir: Directory on the external storage where databases are stored (provided by the IoTool application)

IoToolDataReader db = new IoToolDataReader("AccelerometerX@Device",appDir);

executeSQL: executeSQL(String condition, String order, boolean condensed, int limit):

  • condition: String that is used in "WHERE" statement in sql query. Method automatically pripends "WHERE " if the string is longer than 0.

  • order: String that is used in "ORDER BY" statement in sql query. Method automatically pripends "ORDER BY " if the string is longer than 0.

  • condensed: It condenses the data if true

  • limit (optional): Used in "LIMIT" statement

long now = System.currentTimeMillis();
db.executeSQL("timestamp < " + now + " AND timestamp > " + (now - 1500),"timestamp DESC",false,3);

Moves to the first entry. It return true if it exists.

moveToFirst: moveToFirst()

Moves to the next entry. It return true if it exists.

moveToNext: moveToNext()

Classes

There are three main classes used our demo code package.

Note: Any change to the code may be needed only at the sections which start with "//TODO".

IoToolSensorServiceDemoVS.java

This class defines and implements all necessary setup and processing of data for readings that a sensor supports. In demo code packages this class already implements all service functionality such as reading from database, responding to the application commands etc. This is where you implement logical operation on received data.

package io.senlab.iotool.extension.servicedemovs;

import android.Manifest;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import java.io.File;
import java.lang.ref.WeakReference;
import io.senlab.iotool.library.base.IoToolConstants;
import io.senlab.iotool.library.base.IoToolDataReader;
import io.senlab.iotool.library.base.IoToolReading;


/**
 * Demo service for VS sensors. (c) Senlab.
 */
public class IoToolSensorServiceDemoVS extends Service {
	private static final String serviceName = "IoToolSensorServiceDemoVS";
	protected static final String serviceID = "DemoVS";

	private ConnectServiceCommandReceiver connectReceiver;

	private NotificationManager mNM;
	private Notification notification;
	private PendingIntent contentIntent;

	private boolean broadcastData = false;
	private boolean useDB = false;
	private File appDir;

	private Handler reconnectHandler = new Handler();
	private Runnable reconnectRunnable = null;
	private int reconnectInterval = 1000;

	//TODO define all readings
	private IoToolReading reading1 = null;
	private IoToolReading reading2 = null;

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		try {
				SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

			mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
			CharSequence text = serviceName+" "+getString(R.string.started);
			NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
			notification = builder.setContentIntent(contentIntent)
					.setSmallIcon(R.drawable.status_disconnected)
					.setWhen(System.currentTimeMillis())
					.setAutoCancel(true)
					.setContentTitle(serviceName)
					.setContentText(text)
					.build();
			contentIntent = PendingIntent.getActivity(this, 0, new Intent(), 0);

			startForeground(R.string.foreground_service_id, notification);

			connectReceiver = new ConnectServiceCommandReceiver(this);
			registerReceiver(connectReceiver, new IntentFilter(IoToolConstants.ServiceCommand));

			//TODO get settings provided by preference class
			String inter = sp.getString(getString(R.string.preference_key_interval), getString(R.string.preference_default_value));
			try{
				reconnectInterval = Integer.parseInt(inter);
			}
			catch(Exception e){
				reconnectInterval = 1000;
			}

			broadcastData = intent.getBooleanExtra(IoToolConstants.ServiceSetupBroadcast, false);

			useDB = intent.getBooleanExtra(IoToolConstants.ServiceSetupDB, false);

			appDir = new File(Environment.getExternalStorageDirectory().toString()+"/"+intent.getStringExtra(IoToolConstants.FileDirExtra));
			if (! appDir.exists()) appDir.mkdir();

			boolean permissions = true;
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
				if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) permissions = false;
			}
			if (!permissions) {
				Intent i = new Intent();
				i.setClass(this, IoToolSensorServiceDemoVSPreferences.class);
				i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
				startActivity(i);
			} else {
				initServiceReadings();
				setupServiceReadings(intent);
				reconnectRunnable = new Runnable() {
					@Override
					public void run() {
						queryDB();
						requeryDB();
					}
				};
				requeryDB();
			}
		} catch (Exception e) {
			Log.e("Error",e.toString());
			stop();
			return START_NOT_STICKY;
		}
		//Log.i("TAGG",intent+"");
		return START_STICKY;
	}

	public void onDestroy() {
		super.onDestroy();

		unregisterReceiver(connectReceiver);

		//TODO null all readings
		reading1 = null;
		reading2 = null;
	}

	public IBinder onBind(final Intent intent) {
		return null;
	}

	private void stop() {

		try {
			reconnectHandler.removeCallbacks(reconnectRunnable);
		} catch (Exception e) {
		}
		reconnectRunnable = null;

		//TODO close all readings
		if (reading1.isEnabled()) reading1.close();
		if (reading2.isEnabled()) reading2.close();

		stopForeground(true);

		stopSelf();
	}

	private void writeData(IoToolReading db,long time, double value){
		if(db.isEnabled()){
			db.writeData(time,value);
		}
	}

	private void queryDB() {

		//TODO process received data for each reading. In this example we get value by calling getValue() process it and write into a data base
		try {


			long now = System.currentTimeMillis();

			IoToolDataReader db = new IoToolDataReader("AccelerometerX@Device",appDir);
			db.executeSQL("timestamp < " + now + " AND timestamp > " + (now - 1500),"timestamp DESC",false,3);

			long time = Long.MAX_VALUE;
			double value = 0;

			if(db.moveToFirst()){
				time = db.getTime();
				value = db.getValue();
			}

			while(db.moveToNext()){
				if(time < db.getTime()){
					time = db.getTime();
					value = db.getValue();
				}
				else{
					break;
				}
			}

			if(time != Long.MAX_VALUE) {
				writeData(reading1, now, value);
				writeData(reading2, now, value * 10);
			}
			db.close();

		}catch(Exception e){
			Log.e("Error",e.toString());
		}

	}

	private void requeryDB() {
		reconnectHandler.postDelayed(reconnectRunnable, reconnectInterval);
	}

	private boolean checkFlag(int flag, int flags) {
		if ((flag &amp; flags) != 0) return true;
		else return false;
	}

	private static class ConnectServiceCommandReceiver extends BroadcastReceiver {
		private WeakReference<IoToolSensorServiceDemoVS> w;

		public ConnectServiceCommandReceiver(IoToolSensorServiceDemoVS a) {
			w = new WeakReference<IoToolSensorServiceDemoVS>(a);
		}

		public void onReceive(Context context, Intent intent) {
			if(intent==null || !intent.hasExtra(IoToolConstants.ServiceCommandType))
				return;
			IoToolSensorServiceDemoVS a = w.get();
			if(a!=null) {
				if (intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandStopAllServices) ||
						intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandStopService+serviceID)) {
					a.stop();
				}
				else if(intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandSensorsChanged+serviceID)) {
					a.setupServiceReadings(intent);
				}
			}
		}
	}

	private void initServiceReadings() {
		//TODO initialize all readings
		reading1 = new IoToolReading();
		reading2 = new IoToolReading();
	}

	private void setupServiceReadings(Intent intent) {

		//TODO setup readings
		String typeReading = IoToolReading.dataTypeSingle;
		if(reconnectInterval < 1000){
			typeReading = IoToolReading.dataTypeTimestamps;
		}

		if (intent.getBooleanExtra(IoToolConstants.Data+"Reading1@"+serviceID, false)) {
			reading1.Setup("Reading1@"+serviceID, appDir, useDB, typeReading, 0, broadcastData, true, this);
			reading1.enable();
		} else reading1.disable();

		if (intent.getBooleanExtra(IoToolConstants.Data+"Reading2@"+serviceID, false)) {
			reading2.Setup("Reading2@"+serviceID, appDir, useDB, typeReading, 0, broadcastData, true, this);
			reading2.enable();
		} else reading2.disable();
	}
}

IoToolSensorServiceDemoVSPreferences.java

This class implements preferences for a sensor which are accessible from IoTool app.

package io.senlab.iotool.extension.servicedemovs;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.EditTextPreference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.widget.Toast;

import java.lang.ref.WeakReference;

import io.senlab.iotool.library.base.IoToolConstants;

public class IoToolSensorServiceDemoVSPreferences extends PreferenceActivity {
	private PreferenceManager pm;

	private EditTextPreference SensorVS;

	@Override
    public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
	    // Load the preferences from an XML resource
	    addPreferencesFromResource(R.xml.servicepreferences);

	    pm = getPreferenceManager();

	    initPreferences();

	    loadSettings();
	}

	public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
		switch (requestCode) {
			case IoToolConstants.PERMISSIONS_REQUEST_CODE: {
				boolean alert = false;
				if (grantResults.length > 0) {
					for (int grantResult : grantResults) {
						if (grantResult != PackageManager.PERMISSION_GRANTED) alert = true;
					}
				} else alert = true;
				if (alert) {
					Toast.makeText(getApplicationContext(), IoToolSensorServiceDemoVS.serviceID + " " + getString(R.string.request_permission_explanation), Toast.LENGTH_LONG).show();
					finish();
				}
				return;
			}
		}
	}

	@Override
	public void onResume() {
		super.onResume();

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			boolean requestPermissions = false;
			if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) requestPermissions = true;
			if(checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) requestPermissions = true;
			if(requestPermissions) {
				(new Handler()).postDelayed(new Runnable() {
					@Override
					public void run() {
						if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
							requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.ACCESS_COARSE_LOCATION}, IoToolConstants.PERMISSIONS_REQUEST_CODE);
						}
					}
				}, IoToolConstants.PERMISSIONS_DIALOG_DISPLAY_TIME_MILLIS);
			}
		}
	}

	@Override
	public void onBackPressed() {
		super.onBackPressed();
		saveSettings();
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		new SaveSettingsInBackground(this).execute();
	}

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {

    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
    	super.onConfigurationChanged(newConfig);
    }

	private void initPreferences() {
		//TODO initialize prefrence
		SensorVS = (EditTextPreference) pm.findPreference(getString(R.string.preference_key_interval));
	}

	private void loadSettings() {
        SensorVS.setSummary(getString(R.string.pref_str_summary));
    }

	private void saveSettings() {
		//TODO check for errors in settings
    	try {
    		Long.parseLong(SensorVS.getText());

    		System.gc();
		} catch (Exception e) {
			SensorVS.setText(getString(R.string.preference_default_value));
			System.gc();
		}
    }

    private static class SaveSettingsInBackground extends AsyncTask<Void, Void, Void> {
		private WeakReference<IoToolSensorServiceDemoVSPreferences> w;

		public SaveSettingsInBackground(IoToolSensorServiceDemoVSPreferences a) {
			w = new WeakReference<IoToolSensorServiceDemoVSPreferences>(a);
		}
		protected Void doInBackground(Void... params) {
			IoToolSensorServiceDemoVSPreferences a = w.get();

			if(a!=null) {
				a.saveSettings();
			}
			return null;
		}
    }
}

IoToolSensorServiceDemoVSProvider.java

It must provide info for sensor properties, any dashboard profiles, sensor readings properties. For each of those, info is stored in two arrays, one for headers and one for actual data.

package io.senlab.iotool.extension.servicedemovs;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.net.Uri;

import io.senlab.iotool.library.base.IoToolConstants;
import io.senlab.iotool.library.base.IoToolFunctions;

public class IoToolSensorServiceDemoVSProvider extends ContentProvider {
	static final String PROVIDER_NAME = "io.senlab.iotool.extension.servicedemovs.IoToolSensorServiceDemoVSProvider";

//TODO sensor properties
	static final String[] properties_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_PROPERTIES_ID, IoToolConstants.PROVIDER_PROPERTIES_NAME, IoToolConstants.PROVIDER_PROPERTIES_DEVELOPER};
	static final Object[] properties = new Object[] {1, "DemoVS", "Demo Virtual Sensor", "VSExam"};

//TODO dashboard profiles
	static final String[] profiles_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_PROFILES_NAME, IoToolConstants.PROVIDER_PROFILES_READINGS};
	static final Object[][] profiles = new Object[][] {
			{1, "Demo VS", new String[] {"Reading1@DemoVS", "Reading2@DemoVSS"}}
	};

//TODO readings properties
	static final String[] readings_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_READINGS_ID, IoToolConstants.PROVIDER_READINGS_NAME, IoToolConstants.PROVIDER_READINGS_SHORTNAME, IoToolConstants.PROVIDER_READINGS_UNIT, IoToolConstants.PROVIDER_READINGS_INTERVAL, IoToolConstants.PROVIDER_READINGS_DECIMALS};
	static final Object[][] readings = new Object[][] {
		{1, "Reading1@DemoVS", "Reading 1", "Read.1", "unit1", null, 1},
		{2, "Reading2@DemoVS", "Reading 2", "Read.2", "unit2", null, 1}
	};

	static final int PROPERTIES = 1;
	static final int PROFILES = 2;
	static final int READINGS = 3;

	static final UriMatcher uriMatcher;
	static{
		uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
		uriMatcher.addURI(PROVIDER_NAME, "properties", PROPERTIES);
		uriMatcher.addURI(PROVIDER_NAME, "profiles", PROFILES);
		uriMatcher.addURI(PROVIDER_NAME, "readings", READINGS);
	}

	@Override
	public boolean onCreate() {
		return true;
   	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		throw new SQLException("Failed to add a record into " + uri);
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
		MatrixCursor mc = null;
		switch (uriMatcher.match(uri)){
			case PROPERTIES:
				mc = new MatrixCursor(properties_header);
				mc.addRow(properties);
				break;
			case PROFILES:
				mc = new MatrixCursor(profiles_header);
				for (int i=0; i<profiles.length; i++) mc.addRow(new Object[] {profiles[i][0], profiles[i][1], IoToolFunctions.providerReadingsToString((String[]) profiles[i][2])});
				break;
			case READINGS:
				mc = new MatrixCursor(readings_header);
				for (int i=0; i<readings.length; i++) mc.addRow(new Object[] {readings[i][0], readings[i][1], readings[i][2], readings[i][3], readings[i][4], readings[i][5], readings[i][6]});
				break;
			default:
				throw new IllegalArgumentException("Unknown URI " + uri);
		}
		return mc;
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		return 0;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
		return 0;
	}

	@Override
	public String getType(Uri uri) {
		switch (uriMatcher.match(uri)){
			case PROPERTIES:
				return "vnd.android.cursor.dir/vnd.iotool.properties";
			case PROFILES:
				return "vnd.android.cursor.dir/vnd.iotool.profiles";
			case READINGS:
				return "vnd.android.cursor.dir/vnd.iotool.readings";
			default:
				throw new IllegalArgumentException("Unsupported URI: " + uri);
		}
	}
}