Merge pull request #77 from sbosley/110829_sb_localytics_fixes

Localytics fixes
pull/14/head
Tim Su 13 years ago
commit f5997069cf

@ -21,7 +21,7 @@ import android.text.format.DateUtils;
* 2.0: New upload format. * 2.0: New upload format.
*/ */
//@formatter:on //@formatter:on
public static final String LOCALYTICS_CLIENT_LIBRARY_VERSION = "android_2.1"; //$NON-NLS-1$ public static final String LOCALYTICS_CLIENT_LIBRARY_VERSION = "android_2.2"; //$NON-NLS-1$
/** /**
* The package name of the Localytics library. * The package name of the Localytics library.
@ -72,6 +72,11 @@ import android.text.format.DateUtils;
*/ */
public static boolean ENABLE_PARAMETER_CHECKING = true; public static boolean ENABLE_PARAMETER_CHECKING = true;
/**
* Cached copy of the current Android API level
*
* @see DatapointHelper#getApiLevel()
*/
/*package*/ static final int CURRENT_API_LEVEL = DatapointHelper.getApiLevel(); /*package*/ static final int CURRENT_API_LEVEL = DatapointHelper.getApiLevel();
/** /**

@ -8,6 +8,15 @@
package com.localytics.android; package com.localytics.android;
import android.Manifest.permission;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -18,21 +27,11 @@ import java.math.BigInteger;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import android.Manifest.permission;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.telephony.TelephonyManager;
import android.util.Log;
/** /**
* Provides a number of static functions to aid in the collection and formatting of datapoints. * Provides a number of static functions to aid in the collection and formatting of datapoints.
* <p> * <p>
* Note: this is not a public API. * Note: this is not a public API.
*/ */
@SuppressWarnings("nls")
/* package */final class DatapointHelper /* package */final class DatapointHelper
{ {
/** /**
@ -55,27 +54,39 @@ import android.util.Log;
throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
} }
public static int getApiLevel() /**
* @return current Android API level.
*/
/* package */static int getApiLevel()
{ {
try try
{ {
// Although the Build.VERSION.SDK field has existed since API 1, it is deprecated and could be removed // Although the Build.VERSION.SDK field has existed since API 1, it is deprecated and could be removed
// in the future. Therefore use reflection to retrieve it for maximum forward compatibility. // in the future. Therefore use reflection to retrieve it for maximum forward compatibility.
Class<?> buildClass = Build.VERSION.class; final Class<?> buildClass = Build.VERSION.class;
String sdkString = (String) buildClass.getField("SDK").get(null); // $NON-NLS-1$ final String sdkString = (String) buildClass.getField("SDK").get(null); //$NON-NLS-1$
return Integer.valueOf(sdkString); return Integer.parseInt(sdkString);
} }
catch (Exception e) catch (final Exception e)
{ {
Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
// Although probably not necessary, protects from the aforementioned deprecation // Although probably not necessary, protects from the aforementioned deprecation
try try
{ {
Class<?> buildClass = Build.VERSION.class; final Class<?> buildClass = Build.VERSION.class;
return buildClass.getField("SDK_INT").getInt(null); //$NON-NLS-1$ return buildClass.getField("SDK_INT").getInt(null); //$NON-NLS-1$
} }
catch (Exception ignore) { /**/ } catch (final Exception ignore)
{
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", ignore); //$NON-NLS-1$
}
}
} }
// Worse-case scenario, assume Cupcake
return 3; return 3;
} }
@ -108,7 +119,11 @@ import android.util.Log;
return deviceId; return deviceId;
} }
catch (final FileNotFoundException e) catch (final FileNotFoundException e)
{ // {
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
}
} }
finally finally
{ {
@ -119,7 +134,11 @@ import android.util.Log;
} }
} }
catch (final IOException e) catch (final IOException e)
{ // {
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
}
} }
} }
@ -190,7 +209,7 @@ import android.util.Log;
{ {
final Boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", new Class<?>[] { String.class }, new Object[] { "android.hardware.telephony" }); //$NON-NLS-1$//$NON-NLS-2$ final Boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", new Class<?>[] { String.class }, new Object[] { "android.hardware.telephony" }); //$NON-NLS-1$//$NON-NLS-2$
if (!hasTelephony) if (!hasTelephony.booleanValue())
{ {
if (Constants.IS_LOGGABLE) if (Constants.IS_LOGGABLE)
{ {
@ -236,40 +255,9 @@ import android.util.Log;
*/ */
public static String getTelephonyDeviceIdHashOrNull(final Context context) public static String getTelephonyDeviceIdHashOrNull(final Context context)
{ {
if (Constants.CURRENT_API_LEVEL >= 8) final String id = getTelephonyDeviceIdOrNull(context);
{
final Boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", new Class<?>[] { String.class }, new Object[] { "android.hardware.telephony" }); //$NON-NLS-1$//$NON-NLS-2$
if (!hasTelephony) if (null == id)
{
if (Constants.IS_LOGGABLE)
{
Log.i(Constants.LOG_TAG, "Device does not have telephony; cannot read telephony id"); //$NON-NLS-1$
}
return null;
}
}
/*
* Note: Sometimes Android will deny a package READ_PHONE_STATE permissions, even if the package has the permission. It
* appears to be a race condition that primarily occurs during installation.
*/
String id = null;
if (context.getPackageManager().checkPermission(permission.READ_PHONE_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED)
{
final TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
id = manager.getDeviceId();
}
else
{
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Application does not have permission READ_PHONE_STATE; determining device id is not possible. Please consider requesting READ_PHONE_STATE in the AndroidManifest"); //$NON-NLS-1$
}
}
if (id == null)
{ {
return null; return null;
} }
@ -306,7 +294,8 @@ import android.util.Log;
} }
/** /**
* Gets the device manufacturer's name * Gets the device manufacturer's name. This is only available on SDK 4 or greater, so on SDK 3 this method returns the
* constant string "unknown".
* *
* @return A string naming the manufacturer * @return A string naming the manufacturer
*/ */
@ -317,10 +306,16 @@ import android.util.Log;
{ {
try try
{ {
Class<?> buildClass = Build.class; final Class<?> buildClass = Build.class;
mfg = (String) buildClass.getField("MANUFACTURER").get(null); //$NON-NLS-1$ mfg = (String) buildClass.getField("MANUFACTURER").get(null); //$NON-NLS-1$
} }
catch (Exception ignore) {} catch (final Exception ignore)
{
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", ignore); //$NON-NLS-1$
}
}
} }
return mfg; return mfg;
} }

@ -156,7 +156,7 @@ import org.json.JSONArray;
* <p> * <p>
* SDK compatibility level of the device. * SDK compatibility level of the device.
* *
* @see android.os.Build.VERSION#SDK_INT * @see android.os.Build.VERSION#SDK
*/ */
public static final String KEY_DEVICE_SDK_LEVEL = "dsdk"; //$NON-NLS-1$ public static final String KEY_DEVICE_SDK_LEVEL = "dsdk"; //$NON-NLS-1$

@ -43,9 +43,10 @@ import java.util.Set;
* Version history: * Version history:
* <ol> * <ol>
* <li>1: Initial version</li> * <li>1: Initial version</li>
* <li>2: No format changes--just deleting bad data stranded in the database</li>
* </ol> * </ol>
*/ */
private static final int DATABASE_VERSION = 1; private static final int DATABASE_VERSION = 2;
/** /**
* Singleton instance of the {@link LocalyticsProvider}. Lazily initialized via {@link #getInstance(Context, String)}. * Singleton instance of the {@link LocalyticsProvider}. Lazily initialized via {@link #getInstance(Context, String)}.
@ -82,7 +83,7 @@ import java.util.Set;
* @return An instance of {@link LocalyticsProvider}. * @return An instance of {@link LocalyticsProvider}.
* @throws IllegalArgumentException if {@code context} is null * @throws IllegalArgumentException if {@code context} is null
*/ */
public static LocalyticsProvider getInstance(final Context context, String apiKey) public static LocalyticsProvider getInstance(final Context context, final String apiKey)
{ {
/* /*
* Note: Don't call getApplicationContext() on the context, as that would return a different context and defeat useful * Note: Don't call getApplicationContext() on the context, as that would return a different context and defeat useful
@ -524,15 +525,56 @@ import java.util.Set;
} }
@Override @Override
public void onUpgrade(final SQLiteDatabase arg0, final int oldVersion, final int newVersion) public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion)
{ {
// initial version; no upgrades needed at this time if (1 == oldVersion)
{
// delete stranded sessions that don't have any events
Cursor sessionsCursor = null;
try
{
sessionsCursor = db.query(SessionsDbColumns.TABLE_NAME, new String[]
{ SessionsDbColumns._ID }, null, null, null, null, null);
while (sessionsCursor.moveToNext())
{
Cursor eventsCursor = null;
try
{
String sessionId = Long.toString(sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns._ID)));
eventsCursor = db.query(EventsDbColumns.TABLE_NAME, new String[]
{ EventsDbColumns._ID }, String.format("%s = ?", EventsDbColumns.SESSION_KEY_REF), new String[] //$NON-NLS-1$
{ sessionId }, null, null, null);
if (eventsCursor.getCount() == 0)
{
db.delete(SessionsDbColumns.TABLE_NAME, String.format("%s = ?", SessionsDbColumns._ID), new String[] { sessionId }); //$NON-NLS-1$
}
}
finally
{
if (null != eventsCursor)
{
eventsCursor.close();
eventsCursor = null;
}
}
}
}
finally
{
if (null != sessionsCursor)
{
sessionsCursor.close();
sessionsCursor = null;
}
}
}
} }
// @Override // @Override
// public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) // public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)
// { // {
// // initial version; no downgrades needed at this time
// } // }
} }
@ -541,7 +583,7 @@ import java.util.Set;
* <p> * <p>
* This is not a public API. * This is not a public API.
*/ */
public final class ApiKeysDbColumns implements BaseColumns public static final class ApiKeysDbColumns implements BaseColumns
{ {
/** /**
* Private constructor prevents instantiation * Private constructor prevents instantiation
@ -875,7 +917,7 @@ import java.util.Set;
* <p> * <p>
* Constraints: Must be an integer and cannot be null. * Constraints: Must be an integer and cannot be null.
* *
* @see android.os.Build.VERSION#SDK_INT * @see android.os.Build.VERSION#SDK
*/ */
public static final String ANDROID_SDK = "android_sdk"; //$NON-NLS-1$ public static final String ANDROID_SDK = "android_sdk"; //$NON-NLS-1$

@ -8,6 +8,23 @@
package com.localytics.android; package com.localytics.android;
import android.Manifest.permission;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorJoiner;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -34,23 +51,6 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import android.Manifest.permission;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorJoiner;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import com.localytics.android.JsonObjects.BlobHeader; import com.localytics.android.JsonObjects.BlobHeader;
import com.localytics.android.LocalyticsProvider.ApiKeysDbColumns; import com.localytics.android.LocalyticsProvider.ApiKeysDbColumns;
import com.localytics.android.LocalyticsProvider.AttributesDbColumns; import com.localytics.android.LocalyticsProvider.AttributesDbColumns;
@ -579,12 +579,12 @@ public final class LocalyticsSession
// if less than smallest value // if less than smallest value
if (actualValue < steps[0]) if (actualValue < steps[0])
{ {
bucket = "less than " + steps[0]; //$NON-NLS-1$ bucket = "less than " + steps[0];
} }
// if greater than largest value // if greater than largest value
else if (actualValue >= steps[steps.length - 1]) else if (actualValue >= steps[steps.length - 1])
{ {
bucket = steps[steps.length - 1] + " and above"; //$NON-NLS-1$ bucket = steps[steps.length - 1] + " and above";
} }
else else
{ {
@ -664,6 +664,20 @@ public final class LocalyticsSession
*/ */
public static final int MESSAGE_TAG_SCREEN = 7; public static final int MESSAGE_TAG_SCREEN = 7;
/**
* Sort order for the upload blobs.
* <p>
* This is a workaround for Android bug 3707 <http://code.google.com/p/android/issues/detail?id=3707>.
*/
private static final String UPLOAD_BLOBS_EVENTS_SORT_ORDER = String.format("CAST(%s AS TEXT)", UploadBlobEventsDbColumns.EVENTS_KEY_REF); //$NON-NLS-1$
/**
* Sort order for the events.
* <p>
* This is a workaround for Android bug 3707 <http://code.google.com/p/android/issues/detail?id=3707>.
*/
private static final String EVENTS_SORT_ORDER = String.format("CAST(%s as TEXT)", EventsDbColumns._ID); //$NON-NLS-1$
/** /**
* Application context * Application context
*/ */
@ -952,12 +966,11 @@ public final class LocalyticsSession
return; return;
} }
/* mProvider.runBatchTransaction(new Runnable()
* Update the database. Note that there is a possible data loss condition: if the OPT_OUT flag is written in the API {
* keys table but the process terminates before the opt-out event is written, then the client will stop collecting new @Override
* data but the server won't ever receive the opt-out message which will cause data to be deleted. This is not public void run()
* expected to be likely, and generally still meets user expectations because no new data will be uploaded. {
*/
final ContentValues values = new ContentValues(); final ContentValues values = new ContentValues();
values.put(ApiKeysDbColumns.OPT_OUT, Boolean.valueOf(isOptingOut)); values.put(ApiKeysDbColumns.OPT_OUT, Boolean.valueOf(isOptingOut));
mProvider.update(ApiKeysDbColumns.TABLE_NAME, values, String.format("%s = ?", ApiKeysDbColumns._ID), new String[] { Long.toString(mApiKeyId) }); //$NON-NLS-1$ mProvider.update(ApiKeysDbColumns.TABLE_NAME, values, String.format("%s = ?", ApiKeysDbColumns._ID), new String[] { Long.toString(mApiKeyId) }); //$NON-NLS-1$
@ -975,6 +988,8 @@ public final class LocalyticsSession
{ {
tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null); tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null);
} }
}
});
/* /*
* Update the in-memory representation. It is important for the in-memory representation to be updated after the * Update the in-memory representation. It is important for the in-memory representation to be updated after the
@ -989,13 +1004,13 @@ public final class LocalyticsSession
* <p> * <p>
* This method must only be called after {@link #init()} is called. * This method must only be called after {@link #init()} is called.
* <p> * <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public * Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* interface is to send {@link #MESSAGE_OPEN} to the Handler. * public interface is to send {@link #MESSAGE_OPEN} to the Handler.
* *
* @param ignoreLimits true to ignore limits on the number of sessions. False to enforce limits. * @param ignoreLimits true to ignore limits on the number of sessions. False to enforce limits.
* @see #MESSAGE_OPEN * @see #MESSAGE_OPEN
*/ */
public void open(final boolean ignoreLimits) /* package */void open(final boolean ignoreLimits)
{ {
if (mIsSessionOpen) if (mIsSessionOpen)
{ {
@ -1017,40 +1032,8 @@ public final class LocalyticsSession
} }
/* /*
* Check that the maximum number of sessions hasn't been exceeded * There are two cases: 1. New session and 2. Re-connect to old session. There are two ways to reconnect to an old
*/ * session. One is by the age of the close event, and the other is by the age of the open event.
if (!ignoreLimits)
{
Cursor cursor = null;
try
{
cursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[]
{ SessionsDbColumns._ID }, null, null, null);
if (cursor.getCount() >= Constants.MAX_NUM_SESSIONS)
{
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Maximum number of sessions are already on disk--not writing any new sessions until old sessions are cleared out. Try calling upload() to store more sessions."); //$NON-NLS-1$
}
return;
}
}
finally
{
if (cursor != null)
{
cursor.close();
cursor = null;
}
}
}
mIsSessionOpen = true;
/*
* There are two cases: 1. New session and 2. Re-connect to old session. The way to test whether reconnecting to an
* old session should occur is by the age of the last close event
*/ */
long closeEventId = -1; // sentinel value long closeEventId = -1; // sentinel value
@ -1061,11 +1044,9 @@ public final class LocalyticsSession
try try
{ {
eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[] eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
{ { EventsDbColumns._ID }, String.format("%s = ? AND %s >= ?", EventsDbColumns.EVENT_NAME, EventsDbColumns.WALL_TIME), new String[] { CLOSE_EVENT, Long.toString(System.currentTimeMillis() - Constants.SESSION_EXPIRATION) }, EVENTS_SORT_ORDER); //$NON-NLS-1$
EventsDbColumns._ID,
EventsDbColumns.WALL_TIME }, String.format("%s = ?", EventsDbColumns.EVENT_NAME), new String[] { CLOSE_EVENT }, EventsDbColumns._ID); //$NON-NLS-1$
blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[] blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[]
{ UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UploadBlobEventsDbColumns.EVENTS_KEY_REF); { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UPLOAD_BLOBS_EVENTS_SORT_ORDER);
final int idColumn = eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID); final int idColumn = eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID);
final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[] final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[]
@ -1078,9 +1059,8 @@ public final class LocalyticsSession
{ {
case LEFT: case LEFT:
{ {
if (System.currentTimeMillis() - eventsCursor.getLong(eventsCursor.getColumnIndexOrThrow(EventsDbColumns.WALL_TIME)) < Constants.SESSION_EXPIRATION)
{ if (-1 != closeEventId)
if (closeEventId != -1)
{ {
/* /*
* This should never happen * This should never happen
@ -1089,9 +1069,17 @@ public final class LocalyticsSession
{ {
Log.w(Constants.LOG_TAG, "There were multiple close events within SESSION_EXPIRATION"); //$NON-NLS-1$ Log.w(Constants.LOG_TAG, "There were multiple close events within SESSION_EXPIRATION"); //$NON-NLS-1$
} }
long newClose = eventsCursor.getLong(eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID));
if (newClose > closeEventId)
{
closeEventId = newClose;
}
} }
if (-1 == closeEventId)
{
closeEventId = eventsCursor.getLong(idColumn); closeEventId = eventsCursor.getLong(idColumn);
break;
} }
break; break;
@ -1119,15 +1107,102 @@ public final class LocalyticsSession
} }
} }
if (closeEventId == -1) if (-1 != closeEventId)
{ {
Log.v(Constants.LOG_TAG, "Opening new session"); //$NON-NLS-1$ Log.v(Constants.LOG_TAG, "Opening old closed session and reconnecting"); //$NON-NLS-1$
openNewSession(); mIsSessionOpen = true;
openClosedSession(closeEventId);
} }
else else
{ {
Log.v(Constants.LOG_TAG, "Opening old session and reconnecting"); //$NON-NLS-1$ Cursor sessionsCursor = null;
openOldSession(closeEventId); try
{
sessionsCursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[]
{
SessionsDbColumns._ID,
SessionsDbColumns.SESSION_START_WALL_TIME }, null, null, SessionsDbColumns._ID);
if (sessionsCursor.moveToLast())
{
if (sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns.SESSION_START_WALL_TIME)) >= System.currentTimeMillis()
- Constants.SESSION_EXPIRATION)
{
// reconnect
Log.v(Constants.LOG_TAG, "Opening old unclosed session and reconnecting"); //$NON-NLS-1$
mIsSessionOpen = true;
mSessionId = sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns._ID));
return;
}
// delete empties
Cursor eventsCursor = null;
try
{
String sessionId = Long.toString(sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns._ID)));
eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
{ EventsDbColumns._ID }, String.format("%s = ?", EventsDbColumns.SESSION_KEY_REF), new String[] //$NON-NLS-1$
{ sessionId }, null);
if (eventsCursor.getCount() == 0)
{
mProvider.delete(SessionsDbColumns.TABLE_NAME, String.format("%s = ?", SessionsDbColumns._ID), new String[] { sessionId }); //$NON-NLS-1$
}
}
finally
{
if (null != eventsCursor)
{
eventsCursor.close();
eventsCursor = null;
}
}
}
}
finally
{
if (null != sessionsCursor)
{
sessionsCursor.close();
sessionsCursor = null;
}
}
/*
* Check that the maximum number of sessions hasn't been exceeded
*/
if (!ignoreLimits)
{
Cursor cursor = null;
try
{
cursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[]
{ SessionsDbColumns._ID }, null, null, null);
if (cursor.getCount() >= Constants.MAX_NUM_SESSIONS)
{
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Maximum number of sessions are already on disk--not writing any new sessions until old sessions are cleared out. Try calling upload() to store more sessions."); //$NON-NLS-1$
}
return;
}
}
finally
{
if (cursor != null)
{
cursor.close();
cursor = null;
}
}
}
Log.v(Constants.LOG_TAG, "Opening new session"); //$NON-NLS-1$
mIsSessionOpen = true;
openNewSession();
} }
} }
@ -1138,9 +1213,6 @@ public final class LocalyticsSession
*/ */
private void openNewSession() private void openNewSession()
{ {
// first insert the session
{
final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
final ContentValues values = new ContentValues(); final ContentValues values = new ContentValues();
@ -1191,15 +1263,21 @@ public final class LocalyticsSession
values.put(SessionsDbColumns.NETWORK_COUNTRY, telephonyManager.getNetworkCountryIso()); values.put(SessionsDbColumns.NETWORK_COUNTRY, telephonyManager.getNetworkCountryIso());
values.put(SessionsDbColumns.NETWORK_TYPE, DatapointHelper.getNetworkType(mContext, telephonyManager)); values.put(SessionsDbColumns.NETWORK_TYPE, DatapointHelper.getNetworkType(mContext, telephonyManager));
mProvider.runBatchTransaction(new Runnable()
{
@Override
public void run()
{
mSessionId = mProvider.insert(SessionsDbColumns.TABLE_NAME, values); mSessionId = mProvider.insert(SessionsDbColumns.TABLE_NAME, values);
if (mSessionId == -1) if (mSessionId == -1)
{ {
throw new RuntimeException("session insert failed"); //$NON-NLS-1$ throw new RuntimeException("session insert failed"); //$NON-NLS-1$
} }
}
tagEvent(OPEN_EVENT, null); tagEvent(OPEN_EVENT, null);
}
});
/* /*
* This is placed here so that the DatapointHelper has a chance to retrieve the old UUID before it is deleted. * This is placed here so that the DatapointHelper has a chance to retrieve the old UUID before it is deleted.
@ -1214,10 +1292,8 @@ public final class LocalyticsSession
* @effects Updates the database by deleting the last close event and sets {@link #mSessionId} to the session id of the * @effects Updates the database by deleting the last close event and sets {@link #mSessionId} to the session id of the
* last close event * last close event
*/ */
private void openOldSession(final long closeEventId) private void openClosedSession(final long closeEventId)
{ {
// reconnect old session
Cursor eventCursor = null; Cursor eventCursor = null;
try try
{ {
@ -1259,14 +1335,14 @@ public final class LocalyticsSession
* <p> * <p>
* This method must only be called after {@link #init()} is called. * This method must only be called after {@link #init()} is called.
* <p> * <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public * Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* interface is to send {@link #MESSAGE_CLOSE} to the Handler. * public interface is to send {@link #MESSAGE_CLOSE} to the Handler.
* *
* @see #MESSAGE_OPEN * @see #MESSAGE_OPEN
*/ */
public void close() /* package */void close()
{ {
if (mIsSessionOpen == false) // do nothing if session is not open if (!mIsSessionOpen) // do nothing if session is not open
{ {
if (Constants.IS_LOGGABLE) if (Constants.IS_LOGGABLE)
{ {
@ -1286,15 +1362,15 @@ public final class LocalyticsSession
* <p> * <p>
* This method must only be called after {@link #init()} is called. * This method must only be called after {@link #init()} is called.
* <p> * <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public * Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* interface is to send {@link #MESSAGE_TAG_EVENT} to the Handler. * public interface is to send {@link #MESSAGE_TAG_EVENT} to the Handler.
* *
* @param event The name of the event which occurred. * @param event The name of the event which occurred.
* @param attributes The collection of attributes for this particular event. If this parameter is null, then calling this * @param attributes The collection of attributes for this particular event. If this parameter is null, then calling this
* method has the same effect as calling {@link #tagEvent(String)}. * method has the same effect as calling {@link #tagEvent(String)}.
* @see #MESSAGE_TAG_EVENT * @see #MESSAGE_TAG_EVENT
*/ */
public void tagEvent(final String event, final Map<String, String> attributes) /* package */void tagEvent(final String event, final Map<String, String> attributes)
{ {
if (!mIsSessionOpen) if (!mIsSessionOpen)
{ {
@ -1442,8 +1518,6 @@ public final class LocalyticsSession
cursor = mProvider.query(EventHistoryDbColumns.TABLE_NAME, new String[] cursor = mProvider.query(EventHistoryDbColumns.TABLE_NAME, new String[]
{ EventHistoryDbColumns.NAME }, String.format("%s = ? AND %s = ?", EventHistoryDbColumns.TYPE, EventHistoryDbColumns.SESSION_KEY_REF), new String[] { Integer.toString(EventHistoryDbColumns.TYPE_SCREEN), Long.toString(mSessionId) }, String.format("%s DESC", EventHistoryDbColumns._ID)); //$NON-NLS-1$ //$NON-NLS-2$ { EventHistoryDbColumns.NAME }, String.format("%s = ? AND %s = ?", EventHistoryDbColumns.TYPE, EventHistoryDbColumns.SESSION_KEY_REF), new String[] { Integer.toString(EventHistoryDbColumns.TYPE_SCREEN), Long.toString(mSessionId) }, String.format("%s DESC", EventHistoryDbColumns._ID)); //$NON-NLS-1$ //$NON-NLS-2$
if (cursor.moveToFirst())
{
if (cursor.moveToFirst()) if (cursor.moveToFirst())
{ {
if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME)))) if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME))))
@ -1456,7 +1530,6 @@ public final class LocalyticsSession
} }
} }
} }
}
finally finally
{ {
if (null != cursor) if (null != cursor)
@ -1482,7 +1555,7 @@ public final class LocalyticsSession
/** /**
* Conditionally adds a flow event if no flow event exists in the current upload blob. * Conditionally adds a flow event if no flow event exists in the current upload blob.
*/ */
/* package */void conditionallyAddFlowEvent() private void conditionallyAddFlowEvent()
{ {
/* /*
* Creating a flow "event" is required to act as a placeholder so that the uploader will know that an upload needs to * Creating a flow "event" is required to act as a placeholder so that the uploader will know that an upload needs to
@ -1496,10 +1569,11 @@ public final class LocalyticsSession
try try
{ {
eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[] eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
{ EventsDbColumns._ID }, String.format("%s = ?", EventsDbColumns.EVENT_NAME), new String[] { FLOW_EVENT }, EventsDbColumns._ID); //$NON-NLS-1$ { EventsDbColumns._ID }, String.format("%s = ?", EventsDbColumns.EVENT_NAME), new String[] //$NON-NLS-1$
{ FLOW_EVENT }, EVENTS_SORT_ORDER);
blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[] blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[]
{ UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UploadBlobEventsDbColumns.EVENTS_KEY_REF); { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UPLOAD_BLOBS_EVENTS_SORT_ORDER);
final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[] final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[]
{ EventsDbColumns._ID }, blob_eventsCursor, new String[] { EventsDbColumns._ID }, blob_eventsCursor, new String[]
@ -1547,7 +1621,7 @@ public final class LocalyticsSession
* @effects Mutates the database by creating a new upload blob for all events that are unassociated at the time this * @effects Mutates the database by creating a new upload blob for all events that are unassociated at the time this
* method is called. * method is called.
*/ */
public void preUploadBuildBlobs() /* package */void preUploadBuildBlobs()
{ {
/* /*
* Group all events that aren't part of an upload blob into a new blob. While this process is a linear algorithm that * Group all events that aren't part of an upload blob into a new blob. While this process is a linear algorithm that
@ -1568,11 +1642,10 @@ public final class LocalyticsSession
{ {
EventsDbColumns._ID, EventsDbColumns._ID,
EventsDbColumns.EVENT_NAME, EventsDbColumns.EVENT_NAME,
EventsDbColumns.WALL_TIME }, null, null, EventsDbColumns._ID); EventsDbColumns.WALL_TIME }, null, null, EVENTS_SORT_ORDER);
// eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[] {EventsDbColumns._ID}, String.format("%s != ? AND %s < ?", EventsDbColumns.EVENT_NAME, EventsDbColumns.WALL_TIME), new String[] {CLOSE_EVENT, Long.toString(System.currentTimeMillis() - Constants.SESSION_EXPIRATION)}, EventsDbColumns._ID); //$NON-NLS-1$
blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[] blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[]
{ UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UploadBlobEventsDbColumns.EVENTS_KEY_REF); { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UPLOAD_BLOBS_EVENTS_SORT_ORDER);
final int idColumn = eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID); final int idColumn = eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID);
final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[] final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[]
@ -1648,13 +1721,13 @@ public final class LocalyticsSession
* This method must only be called after {@link #init()} is called. The session does not need to be open for an upload to * This method must only be called after {@link #init()} is called. The session does not need to be open for an upload to
* occur. * occur.
* <p> * <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public * Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* interface is to send {@link #MESSAGE_UPLOAD} to the Handler. * public interface is to send {@link #MESSAGE_UPLOAD} to the Handler.
* *
* @param callback An optional callback to perform once the upload completes. May be null for no callback. * @param callback An optional callback to perform once the upload completes. May be null for no callback.
* @see #MESSAGE_UPLOAD * @see #MESSAGE_UPLOAD
*/ */
public void upload(final Runnable callback) /* package */void upload(final Runnable callback)
{ {
if (sIsUploadingMap.get(mApiKey).booleanValue()) if (sIsUploadingMap.get(mApiKey).booleanValue())
{ {
@ -1671,7 +1744,6 @@ public final class LocalyticsSession
{ {
mProvider.runBatchTransaction(new Runnable() mProvider.runBatchTransaction(new Runnable()
{ {
@Override
public void run() public void run()
{ {
preUploadBuildBlobs(); preUploadBuildBlobs();
@ -1697,7 +1769,7 @@ public final class LocalyticsSession
* Note that a new thread is created for the callback. This ensures that client code can't affect the * Note that a new thread is created for the callback. This ensures that client code can't affect the
* performance of the SessionHandler's thread. * performance of the SessionHandler's thread.
*/ */
new Thread(callback).start(); new Thread(callback, UploadHandler.UPLOAD_CALLBACK_THREAD_NAME).start();
} }
} }
} }
@ -1709,6 +1781,11 @@ public final class LocalyticsSession
/* package */static final class UploadHandler extends Handler /* package */static final class UploadHandler extends Handler
{ {
/**
* Thread name that the upload callback runnable is executed on.
*/
private static final String UPLOAD_CALLBACK_THREAD_NAME = "upload_callback"; //$NON-NLS-1$
/** /**
* Localytics upload URL, as a format string that contains a format for the API key. * Localytics upload URL, as a format string that contains a format for the API key.
*/ */
@ -1800,14 +1877,13 @@ public final class LocalyticsSession
for (final JSONObject json : toUpload) for (final JSONObject json : toUpload)
{ {
builder.append(json.toString()); builder.append(json.toString());
builder.append("\n"); //$NON-NLS-1$ builder.append('\n');
} }
if (uploadSessions(String.format(ANALYTICS_URL, mApiKey), builder.toString())) if (uploadSessions(String.format(ANALYTICS_URL, mApiKey), builder.toString()))
{ {
mProvider.runBatchTransaction(new Runnable() mProvider.runBatchTransaction(new Runnable()
{ {
@Override
public void run() public void run()
{ {
deleteBlobsAndSessions(mProvider); deleteBlobsAndSessions(mProvider);
@ -1824,7 +1900,7 @@ public final class LocalyticsSession
* Execute the callback on a separate thread, to avoid exposing this thread to the client of the * Execute the callback on a separate thread, to avoid exposing this thread to the client of the
* library * library
*/ */
new Thread(callback).start(); new Thread(callback, UPLOAD_CALLBACK_THREAD_NAME).start();
} }
mSessionHandler.sendEmptyMessage(SessionHandler.MESSAGE_UPLOAD_COMPLETE); mSessionHandler.sendEmptyMessage(SessionHandler.MESSAGE_UPLOAD_COMPLETE);
@ -1948,7 +2024,10 @@ public final class LocalyticsSession
} }
catch (final IOException e) catch (final IOException e)
{ {
// there's nothing to be done if this occurs if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
}
} }
} }
} }
@ -2006,7 +2085,11 @@ public final class LocalyticsSession
} }
} }
catch (final JSONException e) catch (final JSONException e)
{ // {
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
}
} }
} }
} }

@ -31,10 +31,9 @@ public final class ReflectionUtils
* @return The result of invoking the named method on the given class for the args * @return The result of invoking the named method on the given class for the args
* @throws RuntimeException if the class or method doesn't exist * @throws RuntimeException if the class or method doesn't exist
*/ */
@SuppressWarnings("unchecked")
public static <T> T tryInvokeStatic(final Class<?> classObject, final String methodName, final Class<?>[] types, final Object[] args) public static <T> T tryInvokeStatic(final Class<?> classObject, final String methodName, final Class<?>[] types, final Object[] args)
{ {
return (T) helper(null, classObject, null, methodName, types, args); return helper(null, classObject, null, methodName, types, args);
} }
/** /**
@ -48,10 +47,9 @@ public final class ReflectionUtils
* @return The result of invoking the named method on the given class for the args * @return The result of invoking the named method on the given class for the args
* @throws RuntimeException if the class or method doesn't exist * @throws RuntimeException if the class or method doesn't exist
*/ */
@SuppressWarnings("unchecked")
public static <T> T tryInvokeStatic(final String className, final String methodName, final Class<?>[] types, final Object[] args) public static <T> T tryInvokeStatic(final String className, final String methodName, final Class<?>[] types, final Object[] args)
{ {
return (T) helper(className, null, null, methodName, types, args); return helper(className, null, null, methodName, types, args);
} }
/** /**
@ -65,10 +63,9 @@ public final class ReflectionUtils
* @return The result of invoking the named method on the given class for the args * @return The result of invoking the named method on the given class for the args
* @throws RuntimeException if the class or method doesn't exist * @throws RuntimeException if the class or method doesn't exist
*/ */
@SuppressWarnings("unchecked")
public static <T> T tryInvokeInstance(final Object target, final String methodName, final Class<?>[] types, final Object[] args) public static <T> T tryInvokeInstance(final Object target, final String methodName, final Class<?>[] types, final Object[] args)
{ {
return (T) helper(target, null, null, methodName, types, args); return helper(target, null, null, methodName, types, args);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

@ -26,8 +26,10 @@ public class StatisticsService {
if(dontCollectStatistics()) if(dontCollectStatistics())
return; return;
if(localyticsSession != null) if(localyticsSession != null) {
localyticsSession.open(); // Multiple calls to open are ok, we just need to make sure it gets reopened after pause
return; return;
}
localyticsSession = new LocalyticsSession(context.getApplicationContext(), localyticsSession = new LocalyticsSession(context.getApplicationContext(),
Constants.LOCALYTICS_KEY); Constants.LOCALYTICS_KEY);
@ -55,9 +57,10 @@ public class StatisticsService {
if(dontCollectStatistics()) if(dontCollectStatistics())
return; return;
if(localyticsSession != null) if(localyticsSession != null) {
localyticsSession.close(); localyticsSession.close();
} }
}
/** /**
* Indicates an error occurred * Indicates an error occurred

Loading…
Cancel
Save