diff --git a/api/src/com/todoroo/andlib/utility/AndroidUtilities.java b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java index 4633d459c..d939484d8 100644 --- a/api/src/com/todoroo/andlib/utility/AndroidUtilities.java +++ b/api/src/com/todoroo/andlib/utility/AndroidUtilities.java @@ -14,9 +14,11 @@ import java.lang.reflect.Method; import java.math.BigInteger; import java.net.URL; import java.net.URLConnection; +import java.net.URLEncoder; import java.security.MessageDigest; import java.util.Arrays; import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; diff --git a/astrid/src/com/todoroo/astrid/dao/ABTestEventDao.java b/astrid/src/com/todoroo/astrid/dao/ABTestEventDao.java index 0083ed19c..7fdd9a63b 100644 --- a/astrid/src/com/todoroo/astrid/dao/ABTestEventDao.java +++ b/astrid/src/com/todoroo/astrid/dao/ABTestEventDao.java @@ -50,8 +50,7 @@ public class ABTestEventDao extends DatabaseDao { int currentTimeIntervalIndex = AndroidUtilities.indexOf( ABTestEvent.TIME_INTERVALS, timeInterval); - if (lastRecordedTimeIntervalIndex < 0 || currentTimeIntervalIndex < 0 - || lastRecordedTimeIntervalIndex >= currentTimeIntervalIndex) + if (lastRecordedTimeIntervalIndex < 0 || currentTimeIntervalIndex < 0) return false; long now = DateUtilities.now(); diff --git a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java index 29ff9241a..db5b81def 100644 --- a/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java +++ b/astrid/src/com/todoroo/astrid/service/AstridDependencyInjector.java @@ -26,6 +26,7 @@ import com.todoroo.astrid.gtasks.GtasksPreferenceService; import com.todoroo.astrid.gtasks.GtasksTaskListUpdater; import com.todoroo.astrid.gtasks.sync.GtasksSyncService; import com.todoroo.astrid.service.abtesting.ABChooser; +import com.todoroo.astrid.service.abtesting.ABTestReporter; import com.todoroo.astrid.service.abtesting.ABTests; import com.todoroo.astrid.service.abtesting.FeatureFlipper; import com.todoroo.astrid.tags.TagService; @@ -63,9 +64,9 @@ public class AstridDependencyInjector extends AbstractDependencyInjector { // com.todoroo.astrid.dao injectables.put("database", Database.class); - injectables.put("taskDao", TaskDao.class); - injectables.put("metadataDao", MetadataDao.class); - injectables.put("tagDataDao", TagDataDao.class); + injectables.put("taskDao", new TaskDao()); + injectables.put("metadataDao", new MetadataDao()); + injectables.put("tagDataDao", new TagDataDao()); injectables.put("storeObjectDao", StoreObjectDao.class); injectables.put("updateDao", UpdateDao.class); injectables.put("userDao", UserDao.class); @@ -101,7 +102,8 @@ public class AstridDependencyInjector extends AbstractDependencyInjector { // AB testing injectables.put("abChooser", ABChooser.class); injectables.put("abTests", new ABTests()); - injectables.put("abTestEventDao", ABTestEventDao.class); + injectables.put("abTestEventDao", new ABTestEventDao()); + injectables.put("abTestReporter", ABTestReporter.class); injectables.put("featureFlipper", FeatureFlipper.class); // com.todoroo.astrid.tags diff --git a/astrid/src/com/todoroo/astrid/service/abtesting/ABTestEventReportingService.java b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestEventReportingService.java new file mode 100644 index 000000000..6e88c5f72 --- /dev/null +++ b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestEventReportingService.java @@ -0,0 +1,119 @@ +package com.todoroo.astrid.service.abtesting; + +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.util.Log; + +import com.todoroo.andlib.data.DatabaseDao.ModelUpdateListener; +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.sql.Query; +import com.todoroo.astrid.dao.ABTestEventDao; +import com.todoroo.astrid.data.ABTestEvent; + +@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"; + + @Autowired + private ABTestEventDao abTestEventDao; + + @Autowired + private ABTestReporter abTestReporter; + + public ABTestEventReportingService() { + DependencyInjectionService.getInstance().inject(this); + } + + public void initialize() { + abTestEventDao.addListener(new ModelUpdateListener() { + @Override + public void onModelUpdated(ABTestEvent model) { + if (model.getValue(ABTestEvent.REPORTED) == 1) + return; + + pushABTestEvent(model); + } + }); + } + + public void pushABTestEvent(final ABTestEvent model) { + new Thread(new Runnable() { + public void run() { + try { + JSONArray payload = new JSONArray().put(jsonFromABTestEvent(model)); + abTestReporter.post(payload); + model.setValue(ABTestEvent.REPORTED, 1); + abTestEventDao.saveExisting(model); + } catch (JSONException e) { + handleException(e); + } catch (IOException e) { + handleException(e); + } + }; + }).start(); + } + + public void pushAllUnreportedABTestEvents() { + TodorooCursor unreported = abTestEventDao.query(Query.select(ABTestEvent.PROPERTIES) + .where(ABTestEvent.REPORTED.eq(0))); + try { + if (unreported.getCount() > 0) { + JSONArray payload = jsonArrayFromABTestEvents(unreported); + abTestReporter.post(payload); + for (unreported.moveToFirst(); !unreported.isAfterLast(); unreported.moveToNext()) { + ABTestEvent model = new ABTestEvent(unreported); + model.setValue(ABTestEvent.REPORTED, 1); + abTestEventDao.saveExisting(model); + } + } + } catch (JSONException e) { + handleException(e); + } catch (IOException e) { + handleException(e); + } finally { + unreported.close(); + } + } + + private void handleException(Exception e) { + Log.e("analytics", "analytics-error", e); + + // TODO: Schedule retry + } + + 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, 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 events) throws JSONException { + JSONArray result = new JSONArray(); + for (events.moveToFirst(); !events.isAfterLast(); events.moveToNext()) { + ABTestEvent model = new ABTestEvent(events); + result.put(jsonFromABTestEvent(model)); + } + return result; + } + +} diff --git a/astrid/src/com/todoroo/astrid/service/abtesting/ABTestReporter.java b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestReporter.java new file mode 100644 index 000000000..b4a1a8183 --- /dev/null +++ b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestReporter.java @@ -0,0 +1,81 @@ +package com.todoroo.astrid.service.abtesting; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.protocol.HTTP; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.andlib.service.RestClient; + +@SuppressWarnings("nls") +public class ABTestReporter { + + /** NOTE: these values are development values and will not work on production */ + private static final String URL = "http://analytics.astrid.com/api/1/retention"; + private static final String API_KEY = "ryyubd"; + private static final String API_SECRET = "q9ef3i"; + + @Autowired private RestClient restClient; + + public ABTestReporter() { + DependencyInjectionService.getInstance().inject(this); + } + + public JSONObject post(JSONArray payload) throws IOException { + try { + HttpEntity postData = createPostData(payload); + if (postData == null) + throw new IOException("Unsupported URL encoding"); + String response = restClient.post(URL, postData); + JSONObject object = new JSONObject(response); + if (object.getString("status").equals("error")) { + throw new IOException("Error reporting ABTestEvent: " + + object.optString("message")); + } + return object; + } catch (JSONException e) { + throw new IOException(e.getMessage()); + } + + } + + private HttpEntity createPostData(JSONArray payload) { + List params = new ArrayList(); + params.add(new BasicNameValuePair("apikey", API_KEY)); + params.add(new BasicNameValuePair("payload", payload.toString())); + + StringBuilder sigBuilder = new StringBuilder(); + for(NameValuePair entry : params) { + if(entry.getValue() == null) + continue; + + String key = entry.getName(); + String value = entry.getValue(); + + sigBuilder.append(key).append(value); + } + + sigBuilder.append(API_SECRET); + String signature = DigestUtils.md5Hex(sigBuilder.toString()); + params.add(new BasicNameValuePair("sig", signature)); + + try { + return new UrlEncodedFormEntity(params, HTTP.UTF_8); + } catch (UnsupportedEncodingException e) { + return null; + } + } + +}