package edu.berkeley.cs.amplab.carat.android.storage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.util.HashMap;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.provider.BaseColumns;
import android.util.Log;
import edu.berkeley.cs.amplab.carat.thrift.Sample;
/**
* Stores samples in a SQLite database until sent. If you add COLUMNS in the
* database, check all the TODO:s for places you need to update.
*
* @author Eemil Lagerspetz
*
*/
public class CaratSampleDB {
private static final String TAG = "CaratSampleDB";
public static final String COLUMN_TIMESTAMP = "timestamp";
public static final String COLUMN_SAMPLE = "sample";
public static final String DATABASE_NAME = "caratdata";
public static final String SAMPLES_VIRTUAL_TABLE = "sampleobjects";
// TODO: Bump version here when changing the protocol, new one incompatible with old
private static final int DATABASE_VERSION = 4;
private static final HashMap<String, String> mColumnMap = buildColumnMap();
private Sample lastSample = null;
private SQLiteDatabase db = null;
private SampleDbOpenHelper helper = null;
private static CaratSampleDB instance = null;
private static Object dbLock = new Object();
public static CaratSampleDB getInstance(Context c) {
if (instance == null) {
instance = new CaratSampleDB(c);
// Log.d("CaratSampleDB", "new instance created and returned");
}
return instance;
}
private CaratSampleDB(Context context) {
synchronized (dbLock) {
helper = new SampleDbOpenHelper(context);
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#finalize()
*/
@Override
protected void finalize() throws Throwable {
synchronized (dbLock) {
if (db != null)
db.close();
}
super.finalize();
}
/**
*
* Builds a map for all columns that may be requested, which will be given
* to the SQLiteQueryBuilder. This is a good way to define aliases for
* column names, but must include all columns, even if the value is the key.
* This allows the ContentProvider to request columns w/o the need to know
* real column names and create the alias itself.
*
* TODO: Needs to be updated when fields update.
*/
private static HashMap<String, String> buildColumnMap() {
HashMap<String, String> map = new HashMap<String, String>();
map.put(COLUMN_TIMESTAMP, COLUMN_TIMESTAMP);
map.put(COLUMN_SAMPLE, COLUMN_SAMPLE);
map.put(BaseColumns._ID, "rowid AS " + BaseColumns._ID);
return map;
}
/**
* Performs a database query.
*
* @param selection
* The selection clause
* @param selectionArgs
* Selection arguments for "?" components in the selection
* @param columns
* The columns to return
* @return A Cursor over all rows matching the query
*/
private Cursor query(String selection, String[] selectionArgs,
String[] columns, String groupBy, String having, String sortOrder) {
/*
* The SQLiteBuilder provides a map for all possible columns requested
* to actual columns in the database, creating a simple column alias
* mechanism by which the ContentProvider does not need to know the real
* column names
*/
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(SAMPLES_VIRTUAL_TABLE);
builder.setProjectionMap(mColumnMap);
Cursor cursor = builder.query(db, columns, selection, selectionArgs,
groupBy, having, sortOrder);
if (cursor == null) {
return null;
} else if (!cursor.moveToFirst()) {
cursor.close();
return null;
}
return cursor;
}
public int countSamples() {
try {
synchronized (dbLock) {
if (db == null || !db.isOpen()) {
try{
db = helper.getWritableDatabase();
}catch (android.database.sqlite.SQLiteException ex){
Log.e(TAG, "Could not open database", ex);
return -1;
}
}
Cursor cursor = db.rawQuery("select count(timestamp) FROM "+SAMPLES_VIRTUAL_TABLE, null);
if (cursor == null) {
// There are no results
return -1;
} else {
int ret = -1;
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
ret = cursor.getInt(0);
cursor.moveToNext();
}
cursor.close();
return ret;
}
}
} catch (Throwable th) {
Log.e(TAG, "Failed to query oldest samples!", th);
}
return -1;
}
public SortedMap<Long, Sample> queryOldestSamples(int howmany) {
SortedMap<Long, Sample> results = new TreeMap<Long, Sample>();
try {
synchronized (dbLock) {
if (db == null || !db.isOpen()) {
try{
db = helper.getWritableDatabase();
}catch (android.database.sqlite.SQLiteException ex){
Log.e(TAG, "Could not open database", ex);
return results;
}
}
String[] columns = mColumnMap.keySet().toArray(
new String[mColumnMap.size()]);
Cursor cursor = query(null, null, columns, null, null,
COLUMN_TIMESTAMP + " ASC LIMIT " + howmany);
if (cursor == null) {
// Log.d("CaratSampleDB", "query returned null");
// There are no results
return results;
} else {
// Log.d("CaratSampleDB", "query is successfull!");
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
Sample s = fillSample(cursor);
if (s != null) {
results.put(cursor.getLong(cursor
.getColumnIndex(BaseColumns._ID)), s);
cursor.moveToNext();
}
}
cursor.close();
}
}
} catch (Throwable th) {
Log.e(TAG, "Failed to query oldest samples!", th);
}
return results;
}
private int delete(String whereClause, String[] whereArgs) {
int deleted = db.delete(SAMPLES_VIRTUAL_TABLE, whereClause, whereArgs);
return deleted;
}
public int deleteSamples(Set<Long> rowids) {
int ret = 0;
try {
synchronized (dbLock) {
if (db == null || !db.isOpen()) {
db = helper.getWritableDatabase();
}
StringBuilder sb = new StringBuilder();
int i = 0;
sb.append("(");
for (Long rowid : rowids) {
sb.append("" + rowid);
i++;
if (i != rowids.size()) {
sb.append(", ");
}
}
sb.append(")");
Log.d("CaratSampleDB",
"Deleting where rowid in " + sb.toString());
ret = delete("rowid in " + sb.toString(), null);
if (db != null && db.isOpen()) {
db.close();
}
}
} catch (Throwable th) {
Log.e(TAG, "Failed to delete samples!", th);
}
return ret;
}
private Sample queryLastSample() {
String[] columns = mColumnMap.keySet().toArray(
new String[mColumnMap.size()]);
Cursor cursor = query(null, null, columns, null, null, COLUMN_TIMESTAMP
+ " DESC LIMIT 1");
if (cursor == null) {
// There are no results
return null;
} else {
cursor.moveToFirst();
if (!cursor.isAfterLast()) {
Sample s = fillSample(cursor);
cursor.close();
lastSample = s;
return s;
}
cursor.close();
return null;
}
}
/*
* Read a sample from the current position of the cursor. TODO: Needs to be
* updated when fields update.
*/
private Sample fillSample(Cursor cursor) {
Sample s = null;
byte[] sampleB = cursor.getBlob(cursor
.getColumnIndex(CaratSampleDB.COLUMN_SAMPLE));
if (sampleB != null) {
ObjectInputStream oi;
try {
oi = new ObjectInputStream(new ByteArrayInputStream(sampleB));
Object o = oi.readObject();
if (o != null)
s = SampleReader.readSample(o);
} catch (StreamCorruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return s;
}
public Sample getLastSample(Context c) {
try {
synchronized (dbLock) {
if (db == null || !db.isOpen()) {
try{
db = helper.getWritableDatabase();
}catch (android.database.sqlite.SQLiteException ex){
Log.e(TAG, "Could not open database", ex);
return lastSample;
}
}
if (lastSample == null)
queryLastSample();
}
} catch (Throwable th) {
Log.e(TAG, "Failed to get last sample!", th);
}
return lastSample;
}
/**
* Store the sample into the database
* @param s the sample to be saved
* @return positive int if the operation is successful, otherwise zero
*/
public long putSample(Sample s) {
long id = 0;
try {
synchronized (dbLock) {
if (db == null || !db.isOpen()) {
db = helper.getWritableDatabase();
}
Log.d(TAG, "CaratSampleDB.putSample(). About to save a sample to the DB. "
+ "uuid=" + s.uuId
+ ", timestamp=" + s.timestamp
+ ", timezone=" + s.timeZone
+ ", batteryLevel=" + s.batteryLevel
+ ", batteryState=" + s.batteryState
+ ", developerMode=" + s.developerMode
+ ", distanceTraveled=" + s.distanceTraveled
+ ", memoryActive=" + s.memoryActive
+ ", memoryFree=" + s.memoryFree
+ ", memoryInactive=" + s.memoryInactive
+ ", memoryWired=" + s.memoryWired
+ ", screenBrightness=" + s.screenBrightness
+ ", networkStatus=" + s.networkStatus
+ ", screenOn=" + s.screenOn
+ ", unknownSources=" + s.unknownSources
+ ", batteryCapacity=" + s.batteryDetails.batteryCapacity
+ ", batteryCharger=" + s.batteryDetails.batteryCharger
+ ", batteryHealth=" + s.batteryDetails.batteryHealth
+ ", batteryTechnology=" + s.batteryDetails.batteryTechnology
+ ", batteryVoltage=" + s.batteryDetails.batteryVoltage
// + ", callStatus=" + s.callInfo.callStatus
// + ", incomingCallTime=" + s.callInfo.incomingCallTime
// + ", nonCallTime=" + s.callInfo.nonCallTime
// + ", outgoingCallTime=" + s.callInfo.outgoingCallTime
+ ", cpuStatus=" + s.cpuStatus
+ ", mobileDataActivity=" + s.getNetworkDetails().mobileDataActivity
+ ", mobileDataStatus=" + s.getNetworkDetails().mobileDataStatus
+ ", mobileNetworkType=" + s.getNetworkDetails().mobileNetworkType
+ ", networkType=" + s.getNetworkDetails().networkType
+ ", wifiLinkSpeed=" + s.getNetworkDetails().wifiLinkSpeed
+ ", wifiSignalStrength=" + s.getNetworkDetails().wifiSignalStrength
+ ", wifiStatus=" + s.getNetworkDetails().wifiStatus
+ ", PiListSize=" + s.getPiListSize()
);
// force init
id = addSample(s);
if (id >= 0) {
lastSample = SampleReader.readSample(s);
}
if (db != null && db.isOpen()) {
db.close();
}
}
} catch (Throwable th) {
Log.e(TAG, "Failed to add a sample!", th);
}
return id;
}
/**
* Add a sample to the database.
*
* @return rowId or -1 if failed
*/
private long addSample(Sample s) {
Log.d("CaratSampleDB.addSample()", "The sample's battery level=" + s.getBatteryLevel());
ContentValues initialValues = new ContentValues();
initialValues.put(COLUMN_TIMESTAMP, s.timestamp);
// Write the sample hashmap as a blob
if (s != null) {
try {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(SampleReader.writeSample(s));
initialValues.put(COLUMN_SAMPLE, bo.toByteArray());
} catch (IOException e) {
e.printStackTrace();
}
}
return db.insert(SAMPLES_VIRTUAL_TABLE, null, initialValues);
}
/**
* This creates/opens the database.
*/
private static class SampleDbOpenHelper extends SQLiteOpenHelper {
private SQLiteDatabase mDatabase;
/*
* Note that FTS3 does not support column constraints and thus, you
* cannot declare a primary key. However, "rowid" is automatically used
* as a unique identifier, so when making requests, we will use "_id" as
* an alias for "rowid"
*/
private static final String FTS_TABLE_CREATE = "CREATE VIRTUAL TABLE "
+ SAMPLES_VIRTUAL_TABLE + " USING fts3 (" + createStatement()
+ ");";
private static final String createStatement() {
Set<String> set = mColumnMap.keySet();
StringBuilder b = new StringBuilder();
int i = 0;
int size = set.size() - 1;
for (String s : set) {
if (s.equals(BaseColumns._ID))
continue;
if (i + 1 == size)
b.append(s);
else
b.append(s + ", ");
i++;
}
return b.toString();
}
SampleDbOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
mDatabase = db;
try {
mDatabase.execSQL(FTS_TABLE_CREATE);
/**
* Compact database here
*/
mDatabase.execSQL("PRAGMA auto_vacuum = 1;");
} catch (Throwable th) {
// Already created
Log.e(TAG, "DB create failed!", th);
}
}
/*
* (non-Javadoc)
*
* @see
* android.database.sqlite.SQLiteOpenHelper#onOpen(android.database.
* sqlite.SQLiteDatabase)
*/
@Override
public void onOpen(SQLiteDatabase db) {
mDatabase = db;
super.onOpen(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + SAMPLES_VIRTUAL_TABLE);
onCreate(db);
}
}
}