/*
* Copyright (C) 2012 University of Washington
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.odk.collect.android.database;
import android.app.Activity;
import android.content.ContentValues;
import android.database.SQLException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import org.javarosa.core.model.FormIndex;
import org.odk.collect.android.application.Collect;
import org.odk.collect.android.logic.FormController;
import java.io.File;
import java.util.Calendar;
import java.util.LinkedList;
/**
* Log all user interface activity into a SQLite database. Logging is disabled by default.
*
* The logging database will be "/sdcard/odk/log/activityLog.db"
*
* Logging is enabled if the file "/sdcard/odk/log/enabled" exists.
*
* @author mitchellsundt@gmail.com
* @author Carl Hartung (carlhartung@gmail.com)
*
*/
public final class ActivityLogger {
private static class DatabaseHelper extends ODKSQLiteOpenHelper {
DatabaseHelper() {
super(Collect.getInstance().getLogPath(), DATABASE_NAME, null, DATABASE_VERSION);
new File(Collect.getInstance().getLogPath()).mkdirs();
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE);
onCreate(db);
}
}
/**
* The minimum delay, in milliseconds, for a scroll action to be considered new.
*/
private static final long MIN_SCROLL_DELAY = 400L;
/**
* The maximum size of the scroll action buffer. After it reaches this size,
* it will be flushed.
*/
private static final int MAX_SCROLL_ACTION_BUFFER_SIZE = 8;
private static final String DATABASE_TABLE = "log";
private static final String ENABLE_LOGGING = "enabled";
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "activityLog.db";
// Database columns
private static final String ID = "_id";
private static final String TIMESTAMP = "timestamp";
private static final String DEVICEID = "device_id";
private static final String CLASS = "class";
private static final String CONTEXT = "context";
private static final String ACTION = "action";
private static final String INSTANCE_PATH = "instance_path";
private static final String QUESTION = "question";
private static final String PARAM1 = "param1";
private static final String PARAM2 = "param2";
private static final String DATABASE_CREATE =
"create table " + DATABASE_TABLE + " (" +
ID + " integer primary key autoincrement, " +
TIMESTAMP + " integer not null, " +
DEVICEID + " text not null, " +
CLASS + " text not null, " +
CONTEXT + " text not null, " +
ACTION + " text, " +
INSTANCE_PATH + " text, " +
QUESTION + " text, " +
PARAM1 + " text, " +
PARAM2 + " text);";
private final boolean mLoggingEnabled;
private final String mDeviceId;
private DatabaseHelper mDbHelper = null;
private SQLiteDatabase mDb = null;
private boolean mIsOpen = false;
// We buffer scroll actions to make sure there aren't too many pauses
// during scrolling. This list is flushed every time any other type of
// action is logged.
private final LinkedList<ContentValues> mScrollActions = new LinkedList<ContentValues>();
public ActivityLogger(String deviceId) {
this.mDeviceId = deviceId;
mLoggingEnabled = new File(Collect.getInstance().getLogPath(), ENABLE_LOGGING).exists();
open();
}
public boolean isOpen() {
return mLoggingEnabled && mIsOpen;
}
public void open() throws SQLException {
if (!mLoggingEnabled || mIsOpen) return;
try {
mDbHelper = new DatabaseHelper();
mDb = mDbHelper.getWritableDatabase();
mIsOpen = true;
} catch (SQLiteException e) {
System.err.println("Error: " + e.getMessage());
mIsOpen = false;
}
}
// cached to improve logging performance...
// only access these through getXPath(FormIndex index);
private FormIndex cachedXPathIndex = null;
private String cachedXPathValue = null;
// DO NOT CALL THIS OUTSIDE OF synchronized(mScrollActions) !!!!
// DO NOT CALL THIS OUTSIDE OF synchronized(mScrollActions) !!!!
// DO NOT CALL THIS OUTSIDE OF synchronized(mScrollActions) !!!!
// DO NOT CALL THIS OUTSIDE OF synchronized(mScrollActions) !!!!
private String getXPath(FormIndex index) {
if ( index == cachedXPathIndex ) return cachedXPathValue;
cachedXPathIndex = index;
cachedXPathValue = Collect.getInstance().getFormController().getXPath(index);
return cachedXPathValue;
}
private void log(String object, String context, String action, String instancePath, FormIndex index, String param1, String param2) {
if (!isOpen()) return;
ContentValues cv = new ContentValues();
cv.put(DEVICEID, mDeviceId);
cv.put(CLASS, object);
cv.put(CONTEXT, context);
cv.put(ACTION, action);
cv.put(INSTANCE_PATH, instancePath);
cv.put(PARAM1, param1);
cv.put(PARAM2, param2);
cv.put(TIMESTAMP, Calendar.getInstance().getTimeInMillis());
insertContentValues(cv, index);
}
public void logScrollAction(Object t, int distance) {
if (!isOpen()) return;
synchronized(mScrollActions) {
long timeStamp = Calendar.getInstance().getTimeInMillis();
// Check to see if we can add this scroll action to the previous action.
if (!mScrollActions.isEmpty()) {
ContentValues lastCv = mScrollActions.get(mScrollActions.size() - 1);
long oldTimeStamp = lastCv.getAsLong(TIMESTAMP);
int oldDistance = Integer.parseInt(lastCv.getAsString(PARAM1));
if (Integer.signum(distance) == Integer.signum(oldDistance) &&
timeStamp - oldTimeStamp < MIN_SCROLL_DELAY) {
lastCv.put(PARAM1, oldDistance + distance);
lastCv.put(TIMESTAMP, timeStamp);
return;
}
}
if (mScrollActions.size() >= MAX_SCROLL_ACTION_BUFFER_SIZE) {
insertContentValues(null, null); // flush scroll list...
}
String idx = "";
String instancePath = "";
FormController formController = Collect.getInstance().getFormController();
if ( formController != null ) {
idx = getXPath(formController.getFormIndex());
instancePath = formController.getInstancePath().getAbsolutePath();
}
// Add a new scroll action to the buffer.
ContentValues cv = new ContentValues();
cv.put(DEVICEID, mDeviceId);
cv.put(CLASS, t.getClass().getName());
cv.put(CONTEXT, "scroll");
cv.put(ACTION, "");
cv.put(PARAM1, distance);
cv.put(QUESTION, idx);
cv.put(INSTANCE_PATH, instancePath);
cv.put(TIMESTAMP, timeStamp);
cv.put(PARAM2, timeStamp);
mScrollActions.add(cv);
}
}
private void insertContentValues(ContentValues cv, FormIndex index) {
synchronized(mScrollActions) {
try {
while ( !mScrollActions.isEmpty() ) {
ContentValues scv = mScrollActions.removeFirst();
mDb.insert(DATABASE_TABLE, null, scv);
}
if ( cv != null ) {
String idx = "";
if ( index != null ) {
idx = getXPath(index);
}
cv.put(QUESTION,idx);
mDb.insert(DATABASE_TABLE, null, cv);
}
} catch (SQLiteConstraintException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
// Convenience methods
public void logOnStart(Activity a) {
log( a.getClass().getName(), "onStart", null, null, null, null, null);
}
public void logOnStop(Activity a) {
log( a.getClass().getName(), "onStop", null, null, null, null, null);
}
public void logAction(Object t, String context, String action) {
log( t.getClass().getName(), context, action, null, null, null, null);
}
public void logActionParam(Object t, String context, String action, String param1) {
log( t.getClass().getName(), context, action, null, null, param1, null);
}
public void logInstanceAction(Object t, String context, String action) {
FormIndex index = null;
String instancePath = null;
FormController formController = Collect.getInstance().getFormController();
if ( formController != null ) {
index = formController.getFormIndex();
File instanceFile = formController.getInstancePath();
if ( instanceFile != null ) {
instancePath = instanceFile.getAbsolutePath();
}
}
log( t.getClass().getName(), context, action, instancePath, index, null, null);
}
public void logInstanceAction(Object t, String context, String action, FormIndex index) {
String instancePath = null;
FormController formController = Collect.getInstance().getFormController();
if ( formController != null ) {
index = formController.getFormIndex();
instancePath = formController.getInstancePath().getAbsolutePath();
}
log( t.getClass().getName(), context, action, instancePath, index, null, null);
}
}