Goodbye flurry, hello localytics, an open-source and friendlier analytics

pull/14/head
Tim Su 13 years ago
parent a9fc2bd41d
commit 64241c5dfe

@ -9,7 +9,6 @@
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="lib" path="libs/annotations.jar"/>
<classpathentry exported="true" kind="lib" path="libs/commons-codec-1.3.jar"/>
<classpathentry exported="true" kind="lib" path="libs/FlurryAgent.jar"/>
<classpathentry exported="true" kind="lib" path="libs/jsr305.jar"/>
<classpathentry exported="true" kind="lib" path="libs/rfc2445-no-joda.jar"/>
<classpathentry exported="true" kind="lib" path="libs/locale_platform.jar"/>

@ -0,0 +1,323 @@
/**
* DatapointHelper.java
* Copyright (C) 2009 Char Software Inc., DBA Localytics
*
* This code is provided under the Localytics Modified BSD License.
* A copy of this license has been distributed in a file called LICENSE
* with this source code.
*
* Please visit www.localytics.com for more information.
*/
package com.localytics.android;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.wifi.WifiManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.provider.Settings.System;
/**
* Provides a number of static functions to aid in the collection and formatting
* of datapoints.
* @author Localytics
*/
public final class DatapointHelper
{
// This class should never be instantiated
private DatapointHelper() { }
private static final String LOG_PREFIX = "(DatapointHelper) ";
private static final String DROID2_ANDROID_ID = "9774d56d682e549c";
// Each YAML entry either goes to the session controller, the event controller
// or the optin controller
public static final String CONTROLLER_SESSION = "- c: se\n";
public static final String CONTROLLER_EVENT = "- c: ev\n";
public static final String CONTROLLER_OPT = "- c: optin\n";
// Each entry is either a create action, or an update action
public static final String ACTION_CREATE = " a: c\n";
public static final String ACTION_UPDATE = " a: u\n";
public static final String ACTION_OPTIN = " a: optin\n";
// The target object for the data being set up.
public static final String OBJECT_SESSION_DP = " se:\n";
public static final String OBJECT_EVENT_DP = " ev:\n";
public static final String OBJECT_OPT = " optin:\n";
// Events can have attributes
public static final String EVENT_ATTRIBUTE = " attrs:\n";
/*******************************
* WEBSERVICE PARAMENTER NAMES *
* *****************************
*/
// Every object has a UUID
public static final String PARAM_UUID = "u";
// The identifier for this application, generated by the user on the webservice
public static final String PARAM_APP_UUID = "au";
// The version of this application, taken from the application's manifest.
public static final String PARAM_APP_VERSION = "av";
// A session's UUID as previously created.
public static final String PARAM_SESSION_UUID = "su";
// A hashed identifier unique to this device
public static final String PARAM_DEVICE_UUID = "du";
// android, iphone, blackberry, windowsmobile
public static final String PARAM_DEVICE_PLATFORM = "dp";
// maker of this device (currently not supported by Android)
public static final String PARAM_DEVICE_MAKE = "dma";
// model of the device
public static final String PARAM_DEVICE_MODEL = "dmo";
// version of the OS on this device
public static final String PARAM_OS_VERSION = "dov";
// country device is from (obtained by querying the SIM card)
public static final String PARAM_DEVICE_COUNTRY = "dc";
// country the current locale is set to
public static final String PARAM_LOCALE_COUNTRY = "dlc";
// country the language is set to
public static final String PARAM_LOCALE_LANGUAGE = "dll";
// Locale as a language_country string. (Not collected because this info
// is already provided by LOCALE_LANGUAGE and LOCALE_COUNTRY.
public static final String PARAM_LOCALE = "dl";
// Country the user is currently in (comes from Sim card)
public static final String PARAM_NETWORK_COUNTRY = "nc";
// Current carrier (comes from sim card)
public static final String PARAM_NETWORK_CARRIER = "nca";
// Current mobile network code (comes from sim card)
public static final String PARAM_NETWORK_MNC = "mnc";
// current mobile country code (comes from sim card)
public static final String PARAM_NETWORK_MCC = "mcc";
// type of data connection (wifi, umts, gprs, evdo, ...)
public static final String PARAM_DATA_CONNECTION = "dac";
// the version of this Localytics client library
public static final String PARAM_LIBRARY_VERSION = "lv";
// The source where the location came from
public static final String PARAM_LOCATION_SOURCE = "ls";
// the latitude returned by the location provider
public static final String PARAM_LOCATION_LAT = "lat";
// the longitude from the location provider
public static final String PARAM_LOCATION_LNG = "lng";
// the current time on the user's device
public static final String PARAM_CLIENT_TIME = "ct";
// sent at closing time, the current time on the users's device
public static final String PARAM_CLIENT_CLOSED_TIME = "ctc";
// The name an event that occured
public static final String PARAM_EVENT_NAME = "n";
// the optin value sent in if a user opts in or out.
public static final String PARAM_OPT_VALUE = "optin";
/**
* Returns the given key/value pair as a YAML string. This string is intended to be
* used to define values for the first level of data in the YAML file. This is
* different from the datapoints which belong another level in.
* @param paramName The name of the parameter
* @param paramValue The value of the parameter
* @param paramIndent The indent level of the parameter
* @return a YAML string which can be dumped to the YAML file
*/
public static String formatYAMLLine(String paramName,String paramValue, int paramIndent)
{
if (paramName.length() > LocalyticsSession.MAX_NAME_LENGTH)
{
Log.v(DatapointHelper.LOG_PREFIX, "Parameter name exceeds "
+ LocalyticsSession.MAX_NAME_LENGTH + " character limit. Truncating.");
paramName = paramName.substring(0, LocalyticsSession.MAX_NAME_LENGTH);
}
if (paramValue.length() > LocalyticsSession.MAX_NAME_LENGTH)
{
Log.v(DatapointHelper.LOG_PREFIX, "Parameter value exceeds "
+ LocalyticsSession.MAX_NAME_LENGTH + " character limit. Truncating.");
paramValue = paramValue.substring(0, LocalyticsSession.MAX_NAME_LENGTH);
}
// The params are stored in the second tier of the YAML data.
// so with spacing, the expected result is: " paramname:paramvalue\n"
StringBuffer formattedString = new StringBuffer();
for (int currentIndent = 0; currentIndent < paramIndent; currentIndent++)
{
formattedString.append(" ");
}
formattedString.append(escapeString(paramName));
formattedString.append(": ");
// Escape the string.
formattedString.append(escapeString(paramValue));
formattedString.append("\n");
return formattedString.toString();
}
/**
* Gets a 1-way hashed value of the device's unique ID. This value is encoded using a SHA-256
* one way hash and cannot be used to determine what device this data came from.
* @param appContext The context used to access the settings resolver
* @return An 1-way hashed identifier unique to this device or null if an ID, or the hashing
* algorithm is not available.
*/
public static String getGlobalDeviceId(final Context appContext)
{
String systemId = System.getString(appContext.getContentResolver(), System.ANDROID_ID);
if(systemId == null || systemId.toLowerCase().equals(DROID2_ANDROID_ID))
{
return null;
}
try
{
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(systemId.getBytes());
BigInteger hashedNumber = new BigInteger(1, digest);
return new String(hashedNumber.toString(16));
}
catch(NoSuchAlgorithmException e)
{
return null;
}
}
/**
* Determines the type of network this device is connected to.
* @param appContext the context used to access the device's WIFI
* @param telephonyManager The manager used to access telephony info
* @return The type of network, or unknown if the information is unavailable
*/
public static String getNetworkType(
final Context appContext,
TelephonyManager telephonyManager)
{
WifiManager wifiManager = (WifiManager)appContext.getSystemService(Context.WIFI_SERVICE);
// this will only work for apps which already have wifi permissions.
try
{
if(wifiManager.isWifiEnabled())
{
return "wifi";
}
}
catch (Exception e) { }
switch (telephonyManager.getNetworkType())
{
case TelephonyManager.NETWORK_TYPE_EDGE : return "edge";
case TelephonyManager.NETWORK_TYPE_GPRS : return "GPRS";
case TelephonyManager.NETWORK_TYPE_UMTS : return "UMTS";
case TelephonyManager.NETWORK_TYPE_UNKNOWN : return "unknown";
}
return "none";
}
/**
* Gets the pretty string for this application's version.
* @param appContext The context used to examine packages
* @return The application's version as a pretty string
*/
public static String getAppVersion(final Context appContext)
{
PackageManager pm = appContext.getPackageManager();
try
{
return pm.getPackageInfo(appContext.getPackageName(), 0).versionName;
}
catch (PackageManager.NameNotFoundException e)
{
return "unknown";
}
}
/**
* Gets the current time, along with local timezone, formatted as a DateTime for the webservice.
* @return a DateTime of the current local time and timezone.
*/
public static String getTimeAsDatetime()
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss-00:00");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
return sdf.format(new Date());
}
/***************************
* Private Helper Functions *
***************************/
/**
* Escapes strings for YAML parser
* @param rawString The string we want to escape.
* @return An escaped string ready for YAML
*/
private static String escapeString(String rawString)
{
StringBuffer parseString = new StringBuffer("\"");
int startRead = 0; // Index to start reading at
int stopRead = 0; // Index characters get read from and where the substring ends
int bufferLength = rawString.length();
if (rawString == null)
{
return "";
}
// Every time we come across a " or \, append what we have so far, append a \,
// then manage our indexes to continue where we left off.
while (stopRead < bufferLength)
{
if (rawString.charAt(stopRead) == '\"' || rawString.charAt(stopRead) == '\\')
{
parseString.append(rawString.substring(startRead, stopRead));
parseString.append('\\');
startRead = stopRead;
}
// Skip null characters.
else if (rawString.charAt(stopRead) == '\0')
{
parseString.append(rawString.substring(startRead, stopRead));
startRead = stopRead + 1;
}
stopRead++;
}
// Append whatever is left after parsing
parseString.append(rawString.substring(startRead, stopRead));
// and finish with a closing "
parseString.append('\"');
return parseString.toString();
}
}

@ -0,0 +1,942 @@
/**
* LocalyticsSession.java
* Copyright (C) 2009 Char Software Inc., DBA Localytics
*
* This code is provided under the Localytics Modified BSD License.
* A copy of this license has been distributed in a file called LICENSE
* with this source code.
*
* Please visit www.localytics.com for more information.
*/
package com.localytics.android;
import android.content.Context;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.os.Build;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.Arrays;
/**
* The class which manages creating, collecting, & uploading a Localytics session.
* Please see the following guides for information on how to best use this
* library, sample code, and other useful information:
* <ul>
* <li><a href="http://wiki.localytics.com/index.php?title=Developer's_Integration_Guide">Main Developer's Integration Guide</a></li>
* <li><a href="http://wiki.localytics.com/index.php?title=Android_2_Minute_Integration">Android 2 minute integration Guide</a></li>
* <li><a href="http://wiki.localytics.com/index.php?title=Android_Integration_Guide">Android Integration Guide</a></li>
* </ul>
* <p>
* Permissions required or recommended for this class:
* <ul>
* <li>android.permission.INTERNET</li> - Required. Necessary to upload data to the webservice.</li>
* <li>android.permission.ACCESS_WIFI_STATE</li> - Optional. Without this users connecting via WIFI will show up as
* having a connection type of 'unknown' on the webservice</li>
* </ul>
*
* <strong>Best Practices</strong>
* <ul>
* <li>Instantiate the LocalyticsSession object in onCreate.</li>
* <li>Create a new LocalyticsSession object, and open it in the onCreaet
* of every activity in your application. This will cause every new
* activity you display to reconnect to the running session.</li>
* <li>Open your session and begin your uploads in onCreate. This way the
* upload has time to complete and it all happens before your users have a
* chance to begin any data intensive actions of their own.</li>
* <li>Close the session in onPause. This is the last terminating function
* which is guaranteed to be called. The final close is the only one
* considered so worrying about activity re-entrance is not a problem.</li>
* <li>Do not call any Localytics functions inside a loop. Instead, calls
* such as <code>tagEvent</code> should follow user actions. This limits the
* amount of data which is stored and uploaded.</li>
* <li>Do not use multiple LocalticsSession objects to upload data with
* multiple application keys. This can cause invalid state.</li>
* </ul>
* @author Localytics
* @version 1.5
*/
public final class LocalyticsSession
{
////////////////////////////////////////
// Member Variables ////////////////////
////////////////////////////////////////
private String _localyticsDirPath; // Path for this app's Localytics Files
private String _sessionFilename = null; // Filename for this session
private String _closeFilename = null; // Filename for this session's close events
private String _sessionUUID; // Unique identifier for this session.
private String _applicationKey; // Unique identifier for the instrumented application
private Context _appContext; // The context used to access device resources
private boolean _isSessionOpen = false; // Whether or not this session has been opened.
private static boolean _isUploading = false; // Only allow one instance of the app to upload at once.
private static boolean _isOptedIn = false; // Optin/out needs to be shared by all instances of this class.
////////////////////////////////////////
// Constants ///////////////////////////
////////////////////////////////////////
private static final String CLIENT_VERSION = "1.5"; // The version of this library.
private static final int MAX_NUM_SESSIONS = 10; // Number of sessions to store on the disk
private static final int MAX_NUM_ATTRIBUTES = 10; // Maximum attributes per event session
protected static final int MAX_NAME_LENGTH = 128; // Maximum characters in an event name or attribute key/value
// Filename and directory constants.
private static final String LOCALYTICS_DIR = "localytics";
private static final String SESSION_FILE_PREFIX = "s_";
private static final String UPLOADER_FILE_PREFIX = "u_";
private static final String CLOSE_FILE_PREFIX = "c_";
private static final String OPTOUT_FILNAME = "opted_out";
private static final String DEVICE_ID_FILENAME = "device_id";
private static final String SESSION_ID_FILENAME = "last_session_id";
// All session opt-in / opt-out events need to be written to same place to gaurantee ordering on the server.
private static final String OPT_SESSION = LocalyticsSession.SESSION_FILE_PREFIX + "opt_session";
// The tag used for identifying Localytics Log messages.
private static final String LOG_TAG = "Localytics_Session";
// The number of milliseconds after which a session is considered closed and can't be reattached to
// 15 seconds * 1000ms
private static int SESSION_EXPIRATION = 15 * 1000;
////////////////////////////////////////
// Public Methods //////////////////////
////////////////////////////////////////
/**
* Creates the Localytics Object. If Localytics is opted out at the time
* this object is created, no data will be collected for the lifetime of
* this session.
* @param appContext The context used to access resources on behalf of the app.
* It is recommended to use <code>getApplicationContext</code> to avoid the potential
* memory leak incurred by maintaining references to activities.
* @param applicationKey The key unique for each application generated
* at www.localytics.com
*/
public LocalyticsSession(final Context appContext, final String applicationKey)
{
this._appContext = appContext;
this._applicationKey = applicationKey;
// Put each application key's files inside a different directory. This
// makes it possible to have multiple app keys inside a single application.
// However, this is not a recommended practice!
this._localyticsDirPath = appContext.getFilesDir() + "/"
+ LocalyticsSession.LOCALYTICS_DIR + "/"
+ this._applicationKey + "/";
// All Localytics API calls are wrapped in try / catch blobs which swallow
// all exceptions. This way if there is a problem with the library the
// integrating application does not crash.
try
{
// If there is an opt-out file, everything is opted out.
File optOutFile = new File(this._localyticsDirPath + LocalyticsSession.OPTOUT_FILNAME);
if(optOutFile.exists())
{
LocalyticsSession._isOptedIn = false;
return;
}
// Otherwise, everything is opted in.
LocalyticsSession._isOptedIn = true;
}
catch (Exception e)
{
Log.v(LocalyticsSession.LOG_TAG, "Swallowing exception: " + e.getMessage());
}
}
/**
* Sets the Localytics Optin state for this application. This
* call is not necessary and is provided for people who wish to allow
* their users the ability to opt out of data collection. It can be
* called at any time. Passing false causes all further data collection
* to stop, and an opt-out event to be sent to the server so the user's
* data is removed from the charts.
* <br>
* There are very serious implications to the quality of your data when
* providing an opt out option. For example, users who have opted out will appear
* as never returning, causing your new/returning chart to skew.
* <br>
* If two instances of the same application are running, and one
* is opted in and the second opts out, the first will also become opted
* out, and neither will collect any more data.
* <br>
* If a session was started while the app was opted out, the session open
* event has already been lost. For this reason, all sessions started
* while opted out will not collect data even after the user opts back in
* or else it taints the comparisons of session lengths and other metrics.
* @param optedIn True if the user wishes to be opted in, false if they
* wish to be opted out and have all their Localytics data deleted.
*/
public void setOptIn(final boolean optedIn)
{
try
{
// Do nothing if optin is unchanged
if(optedIn == LocalyticsSession._isOptedIn)
{
return;
}
LocalyticsSession._isOptedIn = optedIn;
File fp;
if(optedIn == true)
{
// To opt in, delete the opt out file if it exists.
fp = new File(this._localyticsDirPath + LocalyticsSession.OPTOUT_FILNAME);
fp.delete();
createOptEvent(true);
}
else
{
// Create the opt-out file. If it can't be written this is fine because
// it means session files can't be written either so the user is effectively opted out.
fp = new File(this._localyticsDirPath);
fp.mkdirs();
fp = new File(this._localyticsDirPath + LocalyticsSession.OPTOUT_FILNAME);
try
{
fp.createNewFile();
}
catch (IOException e) { }
createOptEvent(false);
}
}
catch (Exception e)
{
Log.v(LocalyticsSession.LOG_TAG, "Swallowing exception: " + e.getMessage());
}
}
/**
* Checks whether or not this session is opted in.
* It is not recommended that an application branch on analytics code
* because this adds an unnecessary testing burden to the developer.
* However, this function is provided for developers who wish to
* pre-populate check boxes in settings menus.
* @return True if the user is opted in, false otherwise.
*/
public boolean isOptedIn()
{
return LocalyticsSession._isOptedIn;
}
/**
* Opens the Localytics session. The session time as presented on the
* website is the time between the first <code>open</code> and the final <code>close</code>
* so it is recommended to open the session as early as possible, and close
* it at the last moment. The session must be opened before any tags can
* be written. It is recommended that this call be placed in <code>onCreate</code>.
* <br>
* If for any reason this is called more than once every subsequent open call
* will be ignored.
* <br>
* For applications with multiple activites, every activity should call <code>open</code>
* in <code>onCreate</code>. This will cause each activity to reconnect to the currently
* running session.
*/
public void open()
{
// Allow only one open call to happen.
synchronized(LocalyticsSession.class)
{
if(LocalyticsSession._isOptedIn == false || // do nothing if opted out
this._isSessionOpen == true) // do nothing if already open
{
Log.v(LocalyticsSession.LOG_TAG, "Session not opened");
return;
}
this._isSessionOpen = true;
}
try
{
// Check if this session was closed within the last 15 seconds. If so, reattach
// to that session rather than open a new one. This will be the case whenever
// a new activity is loaded, or the current one is redrawn. Otherwise, create
// a new session
this._sessionUUID = getOldSessionUUId();
if(this._sessionUUID != null)
{
this._sessionFilename = LocalyticsSession.SESSION_FILE_PREFIX + this._sessionUUID;
this._closeFilename = LocalyticsSession.CLOSE_FILE_PREFIX + this._sessionUUID;;
Log.v(LocalyticsSession.LOG_TAG, "Reconnected to existing session");
}
else
{
// if there are too many files on the disk already, return w/o doing anything.
// All other session calls, such as tagEvent and close will return because isSessionOpen == false
File fp = new File(this._localyticsDirPath);
if(fp.exists())
{
// Get a list of all the session files.
FilenameFilter filter = new FilenameFilter()
{
public boolean accept(File dir, String name)
{
// accept any session or uploader files, but ignore the close files b/c they are tiny.
return (name.startsWith(LocalyticsSession.SESSION_FILE_PREFIX)
|| name.startsWith(LocalyticsSession.UPLOADER_FILE_PREFIX));
}
};
// If that list is larger than the max number, don't create a new session.
if( fp.list(filter).length >= LocalyticsSession.MAX_NUM_SESSIONS)
{
this._isSessionOpen = false;
Log.v(LocalyticsSession.LOG_TAG, "Queue full, session not created");
return;
}
}
// Otherwise, prepare the session.
this._sessionUUID = UUID.randomUUID().toString();
this._sessionFilename = LocalyticsSession.SESSION_FILE_PREFIX + this._sessionUUID;
this._closeFilename = LocalyticsSession.CLOSE_FILE_PREFIX + this._sessionUUID;;
// It isn't necessary to have each session live in its own file because every event
// has the session_id in it. However, this makes maintaining a queue much simpler,
// and it simplifies multithreading because different instances write to different files
fp = getOrCreateFileWithDefaultPath(this._sessionFilename);
if(fp == null)
{
this._isSessionOpen = false;
return;
}
// If the file already exists then an open event has already been written.
else if(fp.length() != 0)
{
Log.v(LocalyticsSession.LOG_TAG, "Session already opened");
return;
}
appendDataToFile(fp, getOpenSessionString());
Log.v(LocalyticsSession.LOG_TAG, "Session opened");
}
}
catch (Exception e)
{
Log.v(LocalyticsSession.LOG_TAG, "Swallowing exception: " + e.getMessage());
}
}
/**
* Closes the Localytics session. This should be done when the application or
* activity is ending. Because of the way the Android lifecycle works, this
* call could end up in a place which gets called multiple times (such as
* <code>onPause</code> which is the recommended location). This is
* fine because only the last close is processed by the server.
* <br>
* Closing does not cause the session to stop collecting data. This is a result
* of the application life cycle. It is possible for onPause to be called long
* before the application is actually ready to close the session.
*/
public void close()
{
if(LocalyticsSession._isOptedIn == false || // do nothing if opted out
this._isSessionOpen == false) // do nothing if session is not open
{
Log.v(LocalyticsSession.LOG_TAG, "Session not closed.");
return;
}
try
{
// Create the session close blob
StringBuffer closeString = new StringBuffer();
closeString.append(DatapointHelper.CONTROLLER_SESSION);
closeString.append(DatapointHelper.ACTION_UPDATE);
closeString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_UUID,
this._sessionUUID,
2));
closeString.append(DatapointHelper.OBJECT_SESSION_DP);
closeString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_APP_UUID,
this._applicationKey,
3));
closeString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_CLIENT_CLOSED_TIME,
DatapointHelper.getTimeAsDatetime(),
3));
// Overwrite the existing close event with the new one
File fp = getOrCreateFileWithDefaultPath(this._closeFilename);
overwriteFile(fp, closeString.toString());
// Write this session id to disk along with a timestamp. This is used to
// determine whether a session is reconnnecting to an existing session or
// being created fresh.
fp = getOrCreateFileWithDefaultPath(LocalyticsSession.SESSION_ID_FILENAME);
overwriteFile(fp,
this._sessionUUID + "\n" + Long.toString(System.currentTimeMillis()));
Log.v(LocalyticsSession.LOG_TAG, "Close event written.");
}
catch (Exception e)
{
Log.v(LocalyticsSession.LOG_TAG, "Swallowing exception: " + e.getMessage());
}
}
/**
* Allows a session to tag a particular event as having occurred. For
* example, if a view has three buttons, it might make sense to tag
* each button click with the name of the button which was clicked.
* For another example, in a game with many levels it might be valuable
* to create a new tag every time the user gets to a new level in order
* to determine how far the average user is progressing in the game.
* <br>
* <strong>Tagging Best Practices</strong>
* <ul>
* <li>DO NOT use tags to record personally identifiable information.</li>
* <li>The best way to use tags is to create all the tag strings as predefined
* constants and only use those. This is more efficient and removes the risk of
* collecting personal information.</li>
* <li>Do not set tags inside loops or any other place which gets called
* frequently. This can cause a lot of data to be stored and uploaded.</li>
* </ul>
* <br>
* @param event The name of the event which occurred.
*/
public void tagEvent(final String event)
{
tagEvent(event, null);
}
/**
* Allows a session to tag a particular event as having occurred, and
* optionally attach a collection of attributes to it. For example, if a
* view has three buttons, it might make sense to tag each button with the
* name of the button which was clicked.
* For another example, in a game with many levels it might be valuable
* to create a new tag every time the user gets to a new level in order
* to determine how far the average user is progressing in the game.
* <br>
* <strong>Tagging Best Practices</strong>
* <ul>
* <li>DO NOT use tags to record personally identifiable information.</li>
* <li>The best way to use tags is to create all the tag strings as predefined
* constants and only use those. This is more efficient and removes the risk of
* collecting personal information.</li>
* <li>Do not set tags inside loops or any other place which gets called
* frequently. This can cause a lot of data to be stored and uploaded.</li>
* </ul>
* <br>
*
* @param event The name of the event which occurred.
* @param attributes The collection of attributes for this particular event.
*/
public void tagEvent(final String event, final Map<String, String> attributes)
{
if(LocalyticsSession._isOptedIn == false || // do nothing if opted out
this._isSessionOpen == false) // do nothing if session is not open
{
Log.v(LocalyticsSession.LOG_TAG, "Tag not written");
return;
}
try
{
// Create the YML for the event
StringBuffer eventString = new StringBuffer();
eventString.append(DatapointHelper.CONTROLLER_EVENT);
eventString.append(DatapointHelper.ACTION_CREATE);
eventString.append(DatapointHelper.OBJECT_EVENT_DP);
eventString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_APP_UUID, this._applicationKey, 3));
eventString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_UUID, UUID.randomUUID().toString(), 3));
eventString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_SESSION_UUID, this._sessionUUID, 3));
eventString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_CLIENT_TIME, DatapointHelper.getTimeAsDatetime(), 3));
eventString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_EVENT_NAME, event, 3));
if (attributes != null)
{
eventString.append(DatapointHelper.EVENT_ATTRIBUTE);
// Iterate through the map's elements and append a line for each one
Iterator<String> attr_it = attributes.keySet().iterator();
for (int currentAttr = 0; attr_it.hasNext() && (currentAttr < MAX_NUM_ATTRIBUTES); currentAttr++)
{
String key = (String) attr_it.next();
String value = (String) attributes.get(key);
eventString.append(DatapointHelper.formatYAMLLine(key, value, 4));
}
}
File fp = getOrCreateFileWithDefaultPath(this._sessionFilename);
appendDataToFile(fp, eventString.toString());
Log.v(LocalyticsSession.LOG_TAG, "Tag written.");
}
catch (Exception e)
{
Log.v(LocalyticsSession.LOG_TAG, "Swallowing exception: " + e.getMessage());
}
}
/**
* Sorts an int value into a set of regular intervals as defined by the
* minimum, maximum, and step size. Both the min and max values are
* inclusive, and in the instance where (max - min + 1) is not evenly
* divisible by step size, the method guarantees only the minimum and the
* step size to be accurate to specification, with the new maximum will be
* moved to the next regular step.
*
* @param actualValue The int value to be sorted.
* @param minValue The int value representing the inclusive minimum interval.
* @param maxValue The int value representing the inclusive maximum interval.
* @param step The int value representing the increment of each interval.
*/
public String createRangedAttribute(int actualValue, int minValue, int maxValue, int step)
{
// Confirm there is at least one bucket
if (step < 1)
{
Log.v(LocalyticsSession.LOG_TAG, "Step must not be less than zero. Returning null.");
return null;
}
if (minValue >= maxValue)
{
Log.v(LocalyticsSession.LOG_TAG, "maxValue must not be less than minValue. Returning null.");
return null;
}
// Determine the number of steps, rounding up using int math
int stepQuantity = (maxValue - minValue + step) / step;
int[] steps = new int[stepQuantity + 1];
for (int currentStep = 0; currentStep <= stepQuantity; currentStep++)
{
steps[currentStep] = minValue + (currentStep) * step;
}
return createRangedAttribute(actualValue, steps);
}
/**
* Sorts an int value into a predefined, pre-sorted set of intervals, returning a string representing the
* new expected value. The array must be sorted in ascending order, with the first element representing
* the inclusive lower bound and the last element representing the exclusive upper bound. For instance,
* the array [0,1,3,10] will provide the following buckets: less than 0, 0, 1-2, 3-9, 10 or greater.
*
* @param actualValue The int value to be bucketed.
* @param steps The sorted int array representing the bucketing intervals.
*/
public String createRangedAttribute(int actualValue, int[] steps)
{
String bucket = null;
// if less than smallest value
if (actualValue < steps[0])
{
bucket = "less than " + steps[0];
}
// if greater than largest value
else if (actualValue >= steps[steps.length - 1])
{
bucket = steps[steps.length - 1] + " and above";
}
else
{
// binarySearch returns the index of the value, or (-(insertion point) - 1) if not found
int bucketIndex = Arrays.binarySearch(steps, actualValue);
if (bucketIndex < 0)
{
// if the index wasn't found, then we want the value before the insertion point as the lower end
// the special case where the insertion point is 0 is covered above, so we don't have to worry about it here
bucketIndex = (- bucketIndex) - 2;
}
if (steps[bucketIndex] == (steps[bucketIndex + 1] - 1))
{
bucket = Integer.toString(steps[bucketIndex]);
}
else
{
bucket = steps[bucketIndex] + "-" + (steps[bucketIndex + 1] - 1);
}
}
return bucket;
}
/**
* Creates a low priority thread which uploads any Localytics data already stored
* on the device. This should be done early in the process life in order to
* guarantee as much time as possible for slow connections to complete. It
* is necessary to do this even if the user has opted out because this is how
* the opt out is transported to the webservice.
*/
public void upload()
{
// Synchronize the check to make sure the upload is not
// already happening. This avoids the possibility of two
// uploader threads being started at once. While this isn't necessary it could
// conceivably reduce the load on the server
synchronized(LocalyticsSession.class)
{
// Uploading should still happen even if the session is opted out.
// This way the opt-out event gets sent to the server so we know this
// user is opted out. After that, no data will be collected so nothing
// will get uploaded.
if(LocalyticsSession._isUploading)
{
return;
}
LocalyticsSession._isUploading = true;
}
try
{
File fp = new File(this._localyticsDirPath);
UploaderThread uploader = new UploaderThread(
fp,
LocalyticsSession.SESSION_FILE_PREFIX,
LocalyticsSession.UPLOADER_FILE_PREFIX,
LocalyticsSession.CLOSE_FILE_PREFIX,
this.uploadComplete);
uploader.start();
}
catch (Exception e)
{
Log.v(LocalyticsSession.LOG_TAG, "Swallowing exception: " + e.getMessage());
}
}
////////////////////////////////////////
// Private Methods /////////////////////
////////////////////////////////////////
/**
* Gets a file from the application storage, or creates if it isn't there.
* The file is created inside this session's default storage location which
* makes it specific to the app key the user selects.
* @param path relative path to create the file in. should not be seperator_terminated
* @param filename the file to create
* @param path to create the file in
* @return a File object, or null if something goes wrong
*/
private File getOrCreateFileWithDefaultPath(final String filename)
{
return getOrCreateFile(filename, this._localyticsDirPath);
}
/**
* Gets a file from the application storage, or creates if it isn't there.
* @param path relative path to create the file in. should not be seperator_terminated
* @param filename the file to create
* @param path to create the file in
* @return a File object, or null if something goes wrong
*/
private File getOrCreateFile(final String filename, final String path)
{
// Get the file if it already exists
File fp = new File(path + filename);
if(fp.exists())
{
return fp;
}
// Otherwise, create any necessary directories, and the file itself.
new File(path).mkdirs();
try
{
if(fp.createNewFile())
{
return fp;
}
}
catch (IOException e)
{
Log.v(LocalyticsSession.LOG_TAG,
"Unable to get or create file: " + filename
+ " in path: " + path);
}
return null;
}
/**
* Uses an OutputStreamWriter to write and flush a string to the end of a text file.
* @param file Text file to append data to.
* @param data String to be appended
*/
private static void appendDataToFile(final File file, final String data)
{
try
{
if(file != null)
{
// Only allow one append to happen at a time. This gaurantees files don't get corrupted by
// multiple threads in the same app writing at the same time, and it gaurantees app-wide
// like device_id don't get broken by multiple instance apps.
synchronized(LocalyticsSession.class)
{
OutputStream out = new FileOutputStream(file, true);
out.write(data.getBytes("UTF8"));
out.close();
}
}
}
catch(IOException e)
{
Log.v(LocalyticsSession.LOG_TAG, "AppendDataToFile failed with IO Exception: " + e.getMessage());
}
}
/**
* Overwrites a given file with new contents
* @param file file to overwrite
* @param contents contents to store in the file
*/
private static void overwriteFile(final File file, final String contents)
{
if(file != null)
{
try
{
FileWriter writer = new FileWriter(file);
writer.write(contents);
writer.flush();
writer.close();
}
catch(IOException e)
{
Log.v(LocalyticsSession.LOG_TAG, "Ovewriting file failed with IO Exception: " + e.getMessage());
}
}
}
/**
* Creates the YAML string for the open session event.
* Collects all the basic session datapoints and writes them out as a YAML string.
* @return The YAML blob for the open session event.
*/
private String getOpenSessionString()
{
StringBuffer openString = new StringBuffer();
TelephonyManager telephonyManager = (TelephonyManager)this._appContext.getSystemService(Context.TELEPHONY_SERVICE);
Locale defaultLocale = Locale.getDefault();
openString.append(DatapointHelper.CONTROLLER_SESSION);
openString.append(DatapointHelper.ACTION_CREATE);
openString.append(DatapointHelper.OBJECT_SESSION_DP);
// Application and session information
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_UUID, this._sessionUUID, 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_APP_UUID, this._applicationKey, 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_APP_VERSION, DatapointHelper.getAppVersion(this._appContext), 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_LIBRARY_VERSION, LocalyticsSession.CLIENT_VERSION, 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_CLIENT_TIME, DatapointHelper.getTimeAsDatetime(), 3));
// Other device information
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_DEVICE_UUID, getDeviceId(), 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_DEVICE_PLATFORM, "Android", 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_OS_VERSION, Build.ID, 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_DEVICE_MODEL, Build.MODEL, 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_LOCALE_LANGUAGE, defaultLocale.getLanguage(), 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_LOCALE_COUNTRY, defaultLocale.getCountry(), 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_DEVICE_COUNTRY, telephonyManager.getSimCountryIso(), 3));
// Network information
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_NETWORK_CARRIER, telephonyManager.getNetworkOperatorName(), 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_NETWORK_COUNTRY, telephonyManager.getNetworkCountryIso(), 3));
openString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_DATA_CONNECTION, DatapointHelper.getNetworkType(this._appContext, telephonyManager), 3));
return openString.toString();
}
/**
* Gets an identifier which is unique to this machine, but generated randomly
* so it can't be traced.
* @return Returns the deviceID as a string
*/
private String getDeviceId()
{
// Try and get the global device ID. If that fails, maintain an id
// local to this application. This way it is still possible to tell things like
// 'new vs returning users' on the webservice.
String deviceId = DatapointHelper.getGlobalDeviceId(this._appContext);
if(deviceId == null)
{
deviceId = getLocalDeviceId();
}
return deviceId;
}
/**
* Gets an identifier unique to this application on this device. If one is not currently available,
* a new one is generated and stored.
* @return An identifier unique to this application this device.
*/
private String getLocalDeviceId()
{
String deviceId = null;
final int bufferSize = 100;
// Open the device ID file. This file is stored at the root level so that
// if an application has multiple app keys (which it shouldn't!) all sessions
// will have a common device_id.
//File fp = getOrCreateFileWithDefaultPath(LocalyticsSession.DEVICE_ID_FILENAME);
File fp = getOrCreateFile(
LocalyticsSession.DEVICE_ID_FILENAME,
this._appContext.getFilesDir() + "/"
+ LocalyticsSession.LOCALYTICS_DIR + "/");
// if the file doesn't exist, create one.
if(fp.length() == 0)
{
deviceId = UUID.randomUUID().toString();
appendDataToFile(fp, deviceId);
}
else
{
try
{
// If it did exist, the file contains the ID.
char[] buf = new char[100];
int numRead;
BufferedReader reader = new BufferedReader(new FileReader(fp), bufferSize);
numRead = reader.read(buf);
deviceId = String.copyValueOf(buf, 0, numRead);
reader.close();
}
catch (FileNotFoundException e) { Log.v(LocalyticsSession.LOG_TAG, "GetLocalDeviceID failed with FNF: " + e.getMessage()); }
catch (IOException e) { Log.v(LocalyticsSession.LOG_TAG, "GetLocalDeviceId Failed with IO Exception: " + e.getMessage()); }
}
return deviceId;
}
/**
* Creates an event telling the webservice that the user opted in or out.
* @param optState True if they opted in, false if they opted out.
*/
private void createOptEvent(boolean optState)
{
File fp = getOrCreateFileWithDefaultPath(LocalyticsSession.OPT_SESSION);
if(fp != null)
{
// Create the session close blob
StringBuffer optString = new StringBuffer();
optString.append(DatapointHelper.CONTROLLER_OPT);
optString.append(DatapointHelper.ACTION_OPTIN);
optString.append(DatapointHelper.OBJECT_OPT);
optString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_DEVICE_UUID,
getDeviceId(),
3));
optString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_APP_UUID,
this._applicationKey,
3));
optString.append(DatapointHelper.formatYAMLLine(
DatapointHelper.PARAM_OPT_VALUE,
Boolean.toString(optState),
3));
appendDataToFile(fp, optString.toString());
}
}
/**
* Returns the UUID of the last session which was closed if the last session
* was closed within 15 seconds. This allows sessions to be shared between
* activities within the same application.
*
* @return the UUID of the previous LocalyticsSession or null if the session has been closed for too long.
*/
private String getOldSessionUUId()
{
final int bufferSize = 100;
// Open the stored session id file
File fp = new File(this._localyticsDirPath + LocalyticsSession.SESSION_ID_FILENAME);
if(fp.exists())
{
try
{
BufferedReader reader = new BufferedReader(new FileReader(fp), bufferSize);
String storedId = reader.readLine();
String timeStamp = reader.readLine();
reader.close();
if(timeStamp != null)
{
// Check if the session happened recently enough
long timeSinceSession = System.currentTimeMillis() - Long.parseLong(timeStamp);
if(SESSION_EXPIRATION > timeSinceSession)
{
return storedId;
}
}
}
catch (FileNotFoundException e)
{
Log.v(LocalyticsSession.LOG_TAG, "File Not Found opening stored session");
return null;
}
catch (IOException e)
{
Log.v(LocalyticsSession.LOG_TAG, "IO Exception getting stored session: " + e.getMessage());
return null;
}
}
return null;
}
/**
* Runnable which gets passed to the uploader thread so it can
* notify the library when uploads are complete.
*/
private Runnable uploadComplete = new Runnable()
{
public void run()
{
LocalyticsSession._isUploading = false;
}
};
}

@ -0,0 +1,347 @@
/**
* UploaderThread.java
* Copyright (C) 2009 Char Software Inc., DBA Localytics
*
* This code is provided under the Localytics Modified BSD License.
* A copy of this license has been distributed in a file called LICENSE
* with this source code.
*
* Please visit www.localytics.com for more information.
*/
package com.localytics.android;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import android.util.Log;
/**
* The thread which handles uploading Localytics data.
* @author Localytics
*/
public class UploaderThread extends Thread
{
private Runnable _completeCallback;
private File _localyticsDir;
private String _sessionFilePrefix;
private String _uploaderFilePrefix;
private String _closeFilePrefix;
// The Tag used in logging.
private final static String LOG_TAG = "Localytics_uploader";
// The URL to send Localytics session data to
private final static String ANALYTICS_URL = "http://analytics.localytics.com/api/datapoints/bulk";
// The size of the buffer used for reading in files.
private final static int BUFFER_SIZE = 1024;
/**
* Creates a thread which uploads the session files in the passed Localytics
* Directory. All files starting with sessionFilePrefix are renamed,
* uploaded and deleted on upload. This way the sessions can continue
* writing data regardless of whether or not the upload succeeds. Files
* which have been renamed still count towards the total number of Localytics
* files which can be stored on the disk.
* @param appContext The context used to access the disk
* @param completeCallback A runnable which is called notifying the caller that upload is complete.
* @param localyticsDir The directory containing the session files
* @param sessionFilePrefix The filename prefix identifying the session files.
* @param uploaderfilePrefix The filename prefixed identifying files to be uploaded.
*/
public UploaderThread(
File localyticsDir,
String sessionFilePrefix,
String uploaderFilePrefix,
String closeFilePrefix,
Runnable completeCallback)
{
this._localyticsDir = localyticsDir;
this._sessionFilePrefix = sessionFilePrefix;
this._uploaderFilePrefix = uploaderFilePrefix;
this._closeFilePrefix = closeFilePrefix;
this._completeCallback = completeCallback;
}
/**
* Renames all the session files (so that other threads can keep writing
* datapoints without affecting the upload. And then uploads them.
*/
public void run()
{
int numFilesToUpload = 0;
try
{
if(this._localyticsDir != null && this._localyticsDir.exists())
{
String basePath = this._localyticsDir.getAbsolutePath();
// rename all the files, taking care to rename the session files
// before the close files.
renameOrAppendSessionFiles(basePath);
renameOrAppendCloseFiles(basePath);
// Grab all the files to be uploaded
FilenameFilter filter = new FilenameFilter()
{
public boolean accept(File dir, String name)
{
return name.startsWith(_uploaderFilePrefix);
}
};
String uploaderFiles[] = this._localyticsDir.list(filter);
numFilesToUpload = uploaderFiles.length;
String postBody = createPostBodyFromFiles(basePath, uploaderFiles);
// Attempt to upload this data. If successful, delete all the uploaderFiles.
Log.v(UploaderThread.LOG_TAG, "Attempting to upload " + numFilesToUpload + " files.");
if(uploadSessions(postBody.toString()) == true)
{
int currentFile;
File uploadedFile;
for(currentFile = 0; currentFile < uploaderFiles.length; currentFile++)
{
uploadedFile = new File(basePath + "/" + uploaderFiles[currentFile]);
uploadedFile.delete();
}
}
}
// Notify the caller the upload is complete.
if(this._completeCallback != null)
{
this._completeCallback.run();
}
}
catch (Exception e)
{
Log.v(UploaderThread.LOG_TAG, "Swallowing exception: " + e.getMessage());
}
}
/**
* Looks at every file whose name starts with the session file prefix
* and renamed or appends it to the appropriately named uploader file.
* @param basePath The full path to the directory containing the files to upload
*/
private void renameOrAppendSessionFiles(String basePath)
{
int currentFile;
// Create a filter to only grab the session files.
FilenameFilter filter = new FilenameFilter()
{
public boolean accept(File dir, String name)
{
return name.startsWith(_sessionFilePrefix);
}
};
// Go through each of the session files
String[] originalFiles = this._localyticsDir.list(filter);
for(currentFile = 0; currentFile < originalFiles.length; currentFile++)
{
String originalFileName = basePath + "/" + originalFiles[currentFile];
String targetFileName = basePath + "/" + this._uploaderFilePrefix + originalFiles[currentFile];
renameOrAppendFile(new File(originalFileName), new File(targetFileName));
}
}
/**
* Looks at every close file in the directory and renames or appends it to
* the appropriate uploader file. This is done separately from the session
* files because it makes life simpler on the webservice if the close events
* come after the session events
* @param basePath The full path to the directory containing the files to upload
*/
private void renameOrAppendCloseFiles(String basePath)
{
int currentFile;
// Create a filter to only grab the session files.
FilenameFilter filter = new FilenameFilter()
{
public boolean accept(File dir, String name)
{
return name.startsWith(_closeFilePrefix);
}
};
// Go through each of the session files
String[] originalFiles = this._localyticsDir.list(filter);
for(currentFile = 0; currentFile < originalFiles.length; currentFile++)
{
String originalFileName = basePath + "/" + originalFiles[currentFile];
// In order for the close events to be appended to the appropriate files
// remove the close prefix and prepend the session prefix
String targetFileName = basePath + "/"
+ this._uploaderFilePrefix
+ getSessionFilenameFromCloseFile(originalFiles[currentFile]);
renameOrAppendFile(new File(originalFileName), new File(targetFileName));
}
}
/**
* Determines what the name of the session file matching this close file would be
* @param closeFilename Name of close file to be used as a guide
* @return The filename of the session which matches this close file
*/
private String getSessionFilenameFromCloseFile(String closeFilename)
{
return this._sessionFilePrefix + closeFilename.substring(this._closeFilePrefix.length());
}
/**
* Checks if destination file exists. If so, it appends the contents of
* source to destination and deletes source. Otherwise, it rename source
* to destination.
* @param source File containing the data to be moved
* @param destination Target for the data
*/
private static void renameOrAppendFile(File source, File destination)
{
if(destination.exists())
{
try
{
InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(destination, true);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0)
{
out.write(buf, 0, len);
}
in.close();
out.close();
source.delete();
}
catch (FileNotFoundException e)
{
Log.v(LOG_TAG, "File not found.");
}
catch (IOException e)
{
Log.v(LOG_TAG, "IO Exception: " + e.getMessage());
}
}
else
{
source.renameTo(destination);
}
}
/**
* Reads in the input files and cats them together in one big string which makes up the
* HTTP request body.
* @param basePath The directory to get the files from
* @param uploaderFiles the list of files to read
* @return A string containing a YML blob which can be uploaded to the webservice.
*/
private String createPostBodyFromFiles(final String basePath, final String[] uploaderFiles)
{
int currentFile;
File inputFile;
StringBuffer postBody = new StringBuffer();
// Read each file in to one buffer. This allows the upload to happen as one
// large transfer instead of many smaller transfers which is preferable on
// a mobile device in which the time required to make a connection is often
// disproportionately large compared to the time to upload the data.
for(currentFile = 0; currentFile < uploaderFiles.length; currentFile++)
{
inputFile = new File(basePath + "/" + uploaderFiles[currentFile]);
try
{
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(inputFile),
"UTF-8"),
UploaderThread.BUFFER_SIZE);
char[] buf = new char[1024];
int numRead;
while( (numRead = reader.read(buf)) > 0)
{
postBody.append(buf, 0, numRead);
}
reader.close();
}
catch (FileNotFoundException e)
{
Log.v(LOG_TAG, "File Not Found");
}
catch (IOException e)
{
Log.v(LOG_TAG, "IOException: " + e.getMessage());
}
}
return postBody.toString();
}
/**
* Uploads the post Body to the webservice
* @param ymlBlob String containing the YML to upload
* @return True on success, false on failure.
*/
private boolean uploadSessions(String ymlBlob)
{
DefaultHttpClient client = new DefaultHttpClient();
HttpPost method = new HttpPost(UploaderThread.ANALYTICS_URL);
try
{
StringEntity postBody = new StringEntity(ymlBlob, "utf8");
method.setEntity(postBody);
HttpResponse response = client.execute(method);
StatusLine status = response.getStatusLine();
Log.v(UploaderThread.LOG_TAG, "Upload complete. Status: " + status.getStatusCode());
// On any response from the webservice, return true so the local files get
// deleted. This avoid an infinite loop in which a bad file keeps getting
// submitted to the webservice time and again.
return true;
}
// return true for any transportation errors.
catch (UnsupportedEncodingException e)
{
Log.v(LOG_TAG, "UnsuppEncodingException: " + e.getMessage());
return false;
}
catch (ClientProtocolException e)
{
Log.v(LOG_TAG, "ClientProtocolException: " + e.getMessage());
return false;
}
catch (IOException e)
{
Log.v(LOG_TAG, "IOException: " + e.getMessage());
return false;
}
}
}

Binary file not shown.

@ -187,8 +187,8 @@ public class FilterListActivity extends ExpandableListActivity {
@Override
protected void onStop() {
super.onStop();
StatisticsService.sessionStop(this);
super.onStop();
}
@Override
@ -200,6 +200,7 @@ public class FilterListActivity extends ExpandableListActivity {
@Override
protected void onPause() {
StatisticsService.sessionPause();
super.onPause();
if(adapter != null)
adapter.unregisterRecevier();

@ -450,8 +450,8 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
@Override
protected void onStop() {
super.onStop();
StatisticsService.sessionStop(this);
super.onStop();
}
@Override
@ -480,6 +480,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
@Override
protected void onPause() {
StatisticsService.sessionPause();
super.onPause();
unregisterReceiver(detailReceiver);
unregisterReceiver(refreshReceiver);

@ -6,13 +6,15 @@ package com.todoroo.astrid.service;
import android.content.Context;
import com.flurry.android.FlurryAgent;
import com.localytics.android.LocalyticsSession;
import com.timsu.astrid.R;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.utility.Constants;
public class StatisticsService {
private static LocalyticsSession localyticsSession;
/**
* Indicate session started
*
@ -22,7 +24,10 @@ public class StatisticsService {
if(dontCollectStatistics())
return;
FlurryAgent.onStartSession(context, Constants.FLURRY_KEY);
localyticsSession = new LocalyticsSession(context.getApplicationContext(),
Constants.LOCALYTICS_KEY);
localyticsSession.open();
localyticsSession.upload();
}
/**
@ -34,21 +39,35 @@ public class StatisticsService {
if(dontCollectStatistics())
return;
FlurryAgent.onEndSession(context);
localyticsSession.upload();
}
public static void reportError(String name, String message, String trace) {
if(dontCollectStatistics())
return;
/**
* Indicate session was paused
*/
public static void sessionPause() {
localyticsSession.close();
}
FlurryAgent.onError(name, message, trace);
/**
* Indicates an error occurred
* @param name
* @param message
* @param trace
*/
public static void reportError(String name, String message, String trace) {
// no reports yet
}
/**
* Indicates an event should be reported
* @param event
*/
public static void reportEvent(String event) {
if(dontCollectStatistics())
return;
FlurryAgent.onEvent(event);
localyticsSession.tagEvent(event);
}
private static boolean dontCollectStatistics() {

@ -6,9 +6,9 @@ public final class Constants {
// --- general application constants
/**
* Flurry API Key
* LCL API Key
*/
public static final String FLURRY_KEY = "T3JAY9TV2JFMJR4YTG16"; //$NON-NLS-1$
public static final String LOCALYTICS_KEY = "ae35a010c66a997ab129ab7-3e2adf46-8bb3-11e0-fe8b-007f58cb3154"; //$NON-NLS-1$
/**
* Application Package

Loading…
Cancel
Save