/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service.abtesting;
import java.io.IOException;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.util.Log;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.dao.ABTestEventDao;
import com.todoroo.astrid.data.ABTestEvent;
import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.service.TaskService;
/**
* Service to manage the reporting of launch events for AB testing.
* On startup, queries the ABTestEvent database for unreported data
* points, merges them into the expected JSONArray format, and
* pushes them to the server.
* @author Sam
*
*/
@SuppressWarnings("nls")
public final class ABTestEventReportingService {
private static final String KEY_TEST = "test";
private static final String KEY_VARIANT = "variant";
private static final String KEY_NEW_USER = "new";
private static final String KEY_ACTIVE_USER = "activated";
private static final String KEY_DAYS = "days";
private static final String KEY_DATE = "date";
private static final String KEY_INITIAL = "initial";
private static final String KEY_ACTIVATION = "activation";
private static final String PREF_REPORTED_ACTIVATION = "p_reported_activation";
@Autowired
private ABTestEventDao abTestEventDao;
@Autowired
private ABTestInvoker abTestInvoker;
@Autowired
private ABTests abTests;
@Autowired
private TaskService taskService;
public ABTestEventReportingService() {
DependencyInjectionService.getInstance().inject(this);
}
/**
* Called on startup from TaskListActivity. Creates any +n days
* launch events that need to be recorded, and pushes all unreported
* data to the server.
*/
public void trackUserRetention(final Context context) {
new Thread(new Runnable() {
@Override
public void run() {
try {
abTestEventDao.createRelativeDateEvents();
pushAllUnreportedABTestEvents();
// reportUserActivation();
} catch (SQLiteException e) {
StartupService.handleSQLiteError(context, e);
}
}
}).start();
}
private void pushAllUnreportedABTestEvents() {
synchronized(ABTestEventReportingService.class) {
if (StatisticsService.dontCollectStatistics())
return;
final TodorooCursor<ABTestEvent> unreported = abTestEventDao.query(Query.select(ABTestEvent.PROPERTIES)
.where(ABTestEvent.REPORTED.eq(0))
.orderBy(Order.asc(ABTestEvent.TEST_NAME), Order.asc(ABTestEvent.TIME_INTERVAL)));
if (unreported.getCount() > 0) {
try {
JSONArray payload = jsonArrayFromABTestEvents(unreported);
abTestInvoker.post(ABTestInvoker.AB_RETENTION_METHOD, payload);
ABTestEvent model = new ABTestEvent();
for (unreported.moveToFirst(); !unreported.isAfterLast(); unreported.moveToNext()) {
model.readFromCursor(unreported);
model.setValue(ABTestEvent.REPORTED, 1);
abTestEventDao.saveExisting(model);
}
} catch (JSONException e) {
handleException(e);
} catch (IOException e) {
handleException(e);
} finally {
unreported.close();
}
}
}
}
private void reportUserActivation() {
synchronized (ABTestEventReportingService.class) {
if (StatisticsService.dontCollectStatistics())
return;
if (Preferences.getBoolean(PREF_REPORTED_ACTIVATION, false) || !taskService.getUserActivationStatus())
return;
final TodorooCursor<ABTestEvent> variants = abTestEventDao.query(Query.select(ABTestEvent.PROPERTIES)
.groupBy(ABTestEvent.TEST_NAME));
try {
JSONArray payload = jsonArrayForActivationAnalytics(variants);
abTestInvoker.post(ABTestInvoker.AB_ACTIVATION_METHOD, payload);
Preferences.setBoolean(PREF_REPORTED_ACTIVATION, true);
} catch (JSONException e) {
handleException(e);
} catch (IOException e) {
handleException(e);
} finally {
variants.close();
}
}
}
public JSONArray getTestsWithVariantsArray() {
JSONArray array = new JSONArray();
Set<String> tests = abTests.getAllTestKeys();
for (String key : tests) {
array.put(key + ":" + abTests.getDescriptionForTestOption(key, ABChooser.readChoiceForTest(key)));
}
return array;
}
private void handleException(Exception e) {
Log.e("analytics", "analytics-error", e);
}
private static JSONObject jsonFromABTestEvent(ABTestEvent model) throws JSONException {
JSONObject payload = new JSONObject();
payload.put(KEY_TEST, model.getValue(ABTestEvent.TEST_NAME));
payload.put(KEY_VARIANT, model.getValue(ABTestEvent.TEST_VARIANT));
payload.put(KEY_NEW_USER, model.getValue(ABTestEvent.NEW_USER) > 0 ? true : false);
payload.put(KEY_ACTIVE_USER, model.getValue(ABTestEvent.ACTIVATED_USER) > 0 ? true : false);
payload.put(KEY_DAYS, new JSONArray().put(model.getValue(ABTestEvent.TIME_INTERVAL)));
long date = model.getValue(ABTestEvent.DATE_RECORDED) / 1000L;
payload.put(KEY_DATE, date);
return payload;
}
private static JSONArray jsonArrayFromABTestEvents(TodorooCursor<ABTestEvent> events) throws JSONException {
JSONArray result = new JSONArray();
String lastTestKey = "";
JSONObject testAcc = null;
ABTestEvent model = new ABTestEvent();
for (events.moveToFirst(); !events.isAfterLast(); events.moveToNext()) {
model.readFromCursor(events);
if (!model.getValue(ABTestEvent.TEST_NAME).equals(lastTestKey)) {
if (testAcc != null)
result.put(testAcc);
testAcc = jsonFromABTestEvent(model);
lastTestKey = model.getValue(ABTestEvent.TEST_NAME);
} else {
int interval = model.getValue(ABTestEvent.TIME_INTERVAL);
if (testAcc != null) { // this should never happen, just stopping the compiler from complaining
JSONArray daysArray = testAcc.getJSONArray(KEY_DAYS);
daysArray.put(interval);
}
}
}
if (testAcc != null)
result.put(testAcc);
return result;
}
private static JSONArray jsonArrayForActivationAnalytics(TodorooCursor<ABTestEvent> events) throws JSONException {
JSONArray result = new JSONArray();
ABTestEvent model = new ABTestEvent();
for (events.moveToFirst(); !events.isAfterLast(); events.moveToNext()) {
model.readFromCursor(events);
JSONObject event = new JSONObject();
event.put(KEY_TEST, model.getValue(ABTestEvent.TEST_NAME));
event.put(KEY_VARIANT, model.getValue(ABTestEvent.TEST_VARIANT));
if (model.getValue(ABTestEvent.ACTIVATED_USER) > 0)
event.put(KEY_INITIAL, true);
else
event.put(KEY_ACTIVATION, true);
result.put(event);
}
return result;
}
}