/************************************************************************************************** * Copyright (C) 2010 Sense Observation Systems, Rotterdam, the Netherlands. All rights reserved. * *************************************************************************************************/ package nl.sense_os.service.storage; import java.nio.BufferOverflowException; import java.util.ArrayList; import nl.sense_os.service.R; import nl.sense_os.service.constants.SensePrefs; import nl.sense_os.service.constants.SensePrefs.Main; import nl.sense_os.service.constants.SensorData.DataPoint; import nl.sense_os.service.provider.SNTP; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.util.Log; /** * Storage for recent sensor data. The data is initially stored in the device's RAM memory. In case * the memory becomes too full, the data is offloaded into a persistent database in the flash * memory. This process is hidden to the end user, so you do not have to worry about which data is * where. * * @author Steven Mulder <steven@sense-os.nl> * * @see ParserUtils * @see DataPoint */ public class LocalStorage { /** * Minimum time to retain data points. If data is not sent to CommonSense, it will be retained * longer. */ private static final long RETENTION_TIME = 1000l * 60 * 60 * 24; /** * Default projection for rows of data points */ private static final String[] DEFAULT_PROJECTION = new String[] { BaseColumns._ID, DataPoint.SENSOR_NAME, DataPoint.DISPLAY_NAME, DataPoint.SENSOR_DESCRIPTION, DataPoint.VALUE_PATH, DataPoint.DATA_TYPE, DataPoint.VALUE, DataPoint.TIMESTAMP, DataPoint.DEVICE_UUID, DataPoint.TRANSMIT_STATE }; private static final int LOCAL_VALUES_URI = 1; private static final int REMOTE_VALUES_URI = 2; private static final String TAG = "LocalStorage"; private static final int DEFAULT_LIMIT = 10000; private static LocalStorage instance; /** * @param context * Context for lazy creating the LocalStorage. * @return Singleton instance of the LocalStorage */ public static LocalStorage getInstance(Context context) { if (null == instance) { instance = new LocalStorage(context.getApplicationContext()); Log.d(TAG, " Local storage has not been created yet"); } return instance; } private final RemoteStorage commonSense; private final SQLiteStorage persisted; private Context context; private LocalStorage(Context context) { Log.i(TAG, "Construct new local storage instance"); this.context = context; persisted = new SQLiteStorage(context); commonSense = new RemoteStorage(context); } public int delete(Uri uri, String where, String[] selectionArgs) { switch (matchUri(uri)) { case LOCAL_VALUES_URI: int nrDeleted = 0; nrDeleted += persisted.delete(where, selectionArgs); return nrDeleted; case REMOTE_VALUES_URI: throw new IllegalArgumentException("Cannot delete values from CommonSense!"); default: throw new IllegalArgumentException("Unknown URI " + uri); } } /** * Removes old data from the persistent storage. * * @return The number of data points deleted */ private int deleteOldData() { Log.i(TAG, "Delete old data points from persistent storage"); // set max retention time long retentionLimit = SNTP.getInstance().getTime() - RETENTION_TIME; // check preferences to see if the data needs to be sent to CommonSense SharedPreferences prefs = context.getSharedPreferences(SensePrefs.MAIN_PREFS, Context.MODE_PRIVATE); boolean useCommonSense = prefs.getBoolean(Main.Advanced.USE_COMMONSENSE, true); String where = null; if (useCommonSense) { // delete data older than maximum retention time if it had been transmitted where = DataPoint.TIMESTAMP + "<" + retentionLimit + " AND " + DataPoint.TRANSMIT_STATE + "==1"; } else { // not using CommonSense: delete all data older than maximum retention time where = DataPoint.TIMESTAMP + "<" + retentionLimit; } int deleted = persisted.delete(where, null); return deleted; } public String getType(Uri uri) { int uriType = matchUri(uri); if (uriType == LOCAL_VALUES_URI || uriType == REMOTE_VALUES_URI) { return DataPoint.CONTENT_TYPE; } else { throw new IllegalArgumentException("Unknown URI " + uri); } } public Uri insert(Uri uri, ContentValues values) { // check the URI switch (matchUri(uri)) { case LOCAL_VALUES_URI: // implementation below break; case REMOTE_VALUES_URI: throw new IllegalArgumentException( "Cannot insert into CommonSense through this ContentProvider"); default: throw new IllegalArgumentException("Unknown URI " + uri); } // insert in the database long rowId = 0; try { rowId = persisted.insert(values); } catch (BufferOverflowException e) { // storage is full! deleteOldData(); // try again rowId = persisted.insert(values); } // notify any listeners (does this work properly?) Uri contentUri = Uri.parse("content://" + context.getString(R.string.local_storage_authority) + DataPoint.CONTENT_URI_PATH); Uri rowUri = ContentUris.withAppendedId(contentUri, rowId); context.getContentResolver().notifyChange(rowUri, null); return rowUri; } public int bulkInsert( ArrayList<ContentValues> values){ return persisted.bulkInsert2(values); } private int matchUri(Uri uri) { if (DataPoint.CONTENT_URI_PATH.equals(uri.getPath())) { return LOCAL_VALUES_URI; } else if (DataPoint.CONTENT_REMOTE_URI_PATH.equals(uri.getPath())) { return REMOTE_VALUES_URI; } else { return -1; } } public Cursor query(Uri uri, String[] projection, String where, String[] selectionArgs, String sortOrder) { return query(uri, projection, where, selectionArgs, DEFAULT_LIMIT, sortOrder); } public Cursor query(Uri uri, String[] projection, String where, String[] selectionArgs, int limit, String sortOrder) { // check URI switch (matchUri(uri)) { case LOCAL_VALUES_URI: // implementation below break; case REMOTE_VALUES_URI: try { return commonSense.query(uri, projection, where, selectionArgs, limit, sortOrder); } catch (Exception e) { Log.e(TAG, "Failed to query the CommonSense data points", e); return null; } default: Log.e(TAG, "Unknown URI: " + uri); throw new IllegalArgumentException("Unknown URI " + uri); } // use default projection if needed if (projection == null) { projection = DEFAULT_PROJECTION; } Cursor persistedCursor = persisted.query(projection, where, selectionArgs, sortOrder); if (persistedCursor.getCount() == 0) { persistedCursor.close(); return null; } return persistedCursor; } public int update(Uri uri, ContentValues newValues, String where, String[] selectionArgs) { // check URI switch (matchUri(uri)) { case LOCAL_VALUES_URI: return persisted.update(newValues, where, selectionArgs); case REMOTE_VALUES_URI: throw new IllegalArgumentException("Cannot update data points in CommonSense"); default: throw new IllegalArgumentException("Unknown URI " + uri); } } }