package interdroid.swan.sensors; import interdroid.swan.swansong.TimestampedValue; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import nl.sense_os.platform.TrivialSensorRegistrator; import nl.sense_os.service.R; import nl.sense_os.service.commonsense.SensorRegistrator; import nl.sense_os.service.constants.SenseDataTypes; import nl.sense_os.service.constants.SensePrefs; import nl.sense_os.service.constants.SensorData.DataPoint; import nl.sense_os.service.storage.LocalStorage; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.util.Log; public abstract class AbstractSwanSensor extends AbstractSensorBase{ public static String TAG = "Abstract Sensor"; /** * The map of values for this sensor. */ private final Map<String, List<TimestampedValue>> values = new HashMap<String, List<TimestampedValue>>(); /** * Sensor specific name, as it will appear on Sense. * Each sensor implementation should set this field */ protected static String SENSOR_NAME; /** * Timestamp indicating when the last flush occurred */ private long mLastFlushed = 0; private long mReadings = 0; private long mLastReadingTimestamp = 0; /** * @return the values */ public final Map<String, List<TimestampedValue>> getValues() { return values; } @Override public void init() { final Context context = this; for (final String valuePath : VALUE_PATHS) { expressionIdsPerValuePath.put(valuePath, new ArrayList<String>()); getValues().put(valuePath, Collections.synchronizedList(new ArrayList<TimestampedValue>())); } new Thread() { @Override public void run() { // register the sensor SensorRegistrator registrator = new TrivialSensorRegistrator(context); for (final String valuePath : VALUE_PATHS) { registrator.checkSensor(SENSOR_NAME, SENSOR_NAME, SenseDataTypes.FLOAT, "valuePath= "+valuePath, "", null, null); } } }.start(); } /** * Adds a value for the given value path to the history. * * @param valuePath * the value path * @param now * the current time * @param value * the value * @param historySize * the history size */ protected final void putValueTrimSize(final String valuePath, final String id, final long now, final Object value /*, final int historySize*/) { updateReadings(now); try { getValues().get(valuePath).add(new TimestampedValue(value, now)); } catch(OutOfMemoryError e){ Log.d(TAG, "OutOfMemoryError"); onDestroySensor(); } checkMemoryAtRuntime(); if (id != null) { notifyDataChangedForId(id); } else { notifyDataChanged(valuePath); } } /** * Check memory used and flush when low on free memory */ static boolean flush = true; public void checkMemoryAtRuntime(){ long usedMemory = Runtime.getRuntime().totalMemory()- Runtime.getRuntime().freeMemory(); if (usedMemory > Runtime.getRuntime().maxMemory() / 5){ if (flush == true){ Log.d(TAG, "Flush to the database"); flush = false; SharedPreferences mainPrefs = getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); String storageOption = mainPrefs.getString(SensePrefs.Main.Advanced.STORAGE, "Remote Storage"); Log.d(TAG, "storage option: " + storageOption); if (0 == storageOption.compareTo("None")) clearData(); else flushData(); } } else { if (flush == false){ Log.d(TAG, "Memory freed"); flush = true; } } } /** * Adds a value for the given value path to the history. * * @param valuePath * the value path * @param now * the current time * @param value * the value * @param historyLength * the history length */ protected final void putValueTrimTime(final String valuePath, final String id, final long now, final Object value, final long historyLength) { updateReadings(now); getValues().get(valuePath).add(new TimestampedValue(value, now)); trimValueByTime(now - historyLength); if (id != null) { notifyDataChangedForId(id); } else { notifyDataChanged(valuePath); } } private void updateReadings(long now) { if (now != mLastReadingTimestamp) { mReadings++; mLastReadingTimestamp = now; } } /** * Trims values past the given expire time. * * @param expire * the time to trim after */ private final void trimValueByTime(final long expire) { for (String valuePath : VALUE_PATHS) { List<TimestampedValue> values = getValues().get(valuePath); while ((values.size() > 0 && values.get(0) .getTimestamp() < expire)) { values.remove(0); } } } @Override public final List<TimestampedValue> getValues(final String id, final long now, final long timespan) { /* * First check if we have all data in memory, otherwise fetch it from upper storage layer */ List<TimestampedValue> valuesForTimeSpan = null; if (mLastFlushed > (now-timespan) ) getLocalValues(now-timespan, mLastFlushed); try { valuesForTimeSpan = getValuesForTimeSpan(getValues().get(registeredValuePaths.get(id)), now, timespan); } catch(OutOfMemoryError e){ Log.e(TAG, "OutOfMemoryError"); onDestroySensor(); } return valuesForTimeSpan; } /** * Store data from memory to local storage. Called when all configurations unregistered from this sensor, * causing the service to destroy. */ public void flushData() { Log.d(TAG, "Flush data to db"); for (final String valuePath: getValues().keySet()){ if (getValues().get(valuePath).size() == 0){ Log.d(TAG, "No values to send for value path" + valuePath); continue; } insertDataInLocalStorage(valuePath); } } /** * Inserts all sensor values from corresponding valuePath into the local database * All values are insert in a single batch * The values are removed from the hash map, clearing memory * @param valuePath */ private void insertDataInLocalStorage(String valuePath){ int size = getValues().get(valuePath).size(); ArrayList<ContentValues> vals = new ArrayList<ContentValues>(); for (int i = size-1; i >= 0; i --){ TimestampedValue tsVal = getValues().get(valuePath).get(i); if (i == size-1){ mLastFlushed = tsVal.getTimestamp(); //the latest timestamp is of the last item in the list } ContentValues val = new ContentValues(); val.put(DataPoint.SENSOR_NAME, SENSOR_NAME); val.put(DataPoint.DISPLAY_NAME, SENSOR_NAME); val.put(DataPoint.SENSOR_DESCRIPTION, "valuePath= "+valuePath); val.put(DataPoint.VALUE_PATH, valuePath); val.put(DataPoint.DATA_TYPE, SenseDataTypes.FLOAT); // val.put(DataPoint.DEVICE_UUID, null); val.put(DataPoint.TIMESTAMP, tsVal.getTimestamp()); val.put(DataPoint.VALUE, tsVal.getValue().toString()); SharedPreferences mainPrefs = getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); String storageOption = mainPrefs.getString(SensePrefs.Main.Advanced.STORAGE, "Remote Storage"); Log.d(TAG, "storage option: " + storageOption); if (0 == storageOption.compareToIgnoreCase("Remote storage")) val.put(DataPoint.TRANSMIT_STATE, 0); else val.put(DataPoint.TRANSMIT_STATE, 1); vals.add(val); } getValues().get(valuePath).clear(); bulkInsertToLocalStorage(vals); } /** * Insert data in the database in a separate thread * @param cvalues -data to be inserted */ public void bulkInsertToLocalStorage(final ArrayList<ContentValues> cvalues) { final int sizeToInsert = cvalues.size(); new Thread() { @Override public void run() { int count = LocalStorage.getInstance(getApplicationContext()).bulkInsert(cvalues); if (count == sizeToInsert){ Log.d(TAG, "data (count = " + count + ") flushed successfully"); } else Log.d(TAG, "inserted " + count + " elements in the db instead of " + sizeToInsert); } }.start(); } /** * Get data from the db, collected in the period (start,end) and put it in the hash map with values */ protected void getLocalValues(final long start, final long end) { try { String[] projection = new String[] {DataPoint.VALUE_PATH, DataPoint.TIMESTAMP, DataPoint.VALUE }; String where = "(" + DataPoint.TIMESTAMP + " >= " + String.valueOf(start) + " AND " + DataPoint.TIMESTAMP + " <= " + String.valueOf(end) + ")"; Uri uri = Uri.parse("content://" + getResources().getString(R.string.local_storage_authority) + DataPoint.CONTENT_URI_PATH); //sort order matter because latest values should go last String sortOrder = DataPoint.TIMESTAMP + " ASC"; Cursor c = LocalStorage.getInstance(getApplicationContext()).query(uri, projection, where, null, sortOrder); if (null == c || !c.moveToFirst()){ Log.d(TAG, "Nothing in the db"); //fetch it from remote storage? return; } Log.d(TAG, c.getCount()+" items retrieved from db"); while (!c.isAfterLast()) { getValues().get(c.getString(c.getColumnIndex(DataPoint.VALUE_PATH))) .add(new TimestampedValue(c.getString(c.getColumnIndex(DataPoint.VALUE)), c.getLong(c.getColumnIndex(DataPoint.TIMESTAMP)))); c.moveToNext(); } c.close(); } catch (IllegalStateException e) { Log.w(TAG, "Failed to query remote data", e); } } @Override public long getReadings() { return mReadings; } /** * Checks whether there are any readings in memory */ public boolean isMemoryEmpty(){ for (List<TimestampedValue> readingsList : getValues().values()) { if (!readingsList.isEmpty()) return false; } return true; } /** * Clears the values from the lit */ private void clearData(){ for (List<TimestampedValue> readingsList : getValues().values()) readingsList.clear(); } @Override public void onDestroySensor() { if (registeredConfigurations.size() == 0) flushData(); } }