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.
*/
//@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.
@ -72,6 +72,11 @@ import android.text.format.DateUtils;
*/
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();
/**

@ -8,6 +8,15 @@
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.File;
import java.io.FileNotFoundException;
@ -18,21 +27,11 @@ import java.math.BigInteger;
import java.security.MessageDigest;
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.
* <p>
* Note: this is not a public API.
*/
@SuppressWarnings("nls")
/* package */final class DatapointHelper
{
/**
@ -55,27 +54,39 @@ import android.util.Log;
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
{
// 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.
Class<?> buildClass = Build.VERSION.class;
String sdkString = (String) buildClass.getField("SDK").get(null); // $NON-NLS-1$
return Integer.valueOf(sdkString);
final Class<?> buildClass = Build.VERSION.class;
final String sdkString = (String) buildClass.getField("SDK").get(null); //$NON-NLS-1$
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
try
{
Class<?> buildClass = Build.VERSION.class;
final Class<?> buildClass = Build.VERSION.class;
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;
}
@ -108,7 +119,11 @@ import android.util.Log;
return deviceId;
}
catch (final FileNotFoundException e)
{ //
{
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$
}
}
finally
{
@ -119,7 +134,11 @@ import android.util.Log;
}
}
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$
if (!hasTelephony)
if (!hasTelephony.booleanValue())
{
if (Constants.IS_LOGGABLE)
{
@ -236,40 +255,9 @@ import android.util.Log;
*/
public static String getTelephonyDeviceIdHashOrNull(final Context context)
{
if (Constants.CURRENT_API_LEVEL >= 8)
{
final Boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", new Class<?>[] { String.class }, new Object[] { "android.hardware.telephony" }); //$NON-NLS-1$//$NON-NLS-2$
final String id = getTelephonyDeviceIdOrNull(context);
if (!hasTelephony)
{
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)
if (null == id)
{
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
*/
@ -317,10 +306,16 @@ import android.util.Log;
{
try
{
Class<?> buildClass = Build.class;
final Class<?> buildClass = Build.class;
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;
}

@ -156,7 +156,7 @@ import org.json.JSONArray;
* <p>
* 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$

@ -43,9 +43,10 @@ import java.util.Set;
* Version history:
* <ol>
* <li>1: Initial version</li>
* <li>2: No format changes--just deleting bad data stranded in the database</li>
* </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)}.
@ -82,7 +83,7 @@ import java.util.Set;
* @return An instance of {@link LocalyticsProvider}.
* @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
@ -524,15 +525,56 @@ import java.util.Set;
}
@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
// 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>
* This is not a public API.
*/
public final class ApiKeysDbColumns implements BaseColumns
public static final class ApiKeysDbColumns implements BaseColumns
{
/**
* Private constructor prevents instantiation
@ -875,7 +917,7 @@ import java.util.Set;
* <p>
* 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$

@ -8,6 +8,23 @@
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.IOException;
import java.io.UnsupportedEncodingException;
@ -34,23 +51,6 @@ import org.json.JSONArray;
import org.json.JSONException;
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.LocalyticsProvider.ApiKeysDbColumns;
import com.localytics.android.LocalyticsProvider.AttributesDbColumns;
@ -579,12 +579,12 @@ public final class LocalyticsSession
// if less than smallest value
if (actualValue < steps[0])
{
bucket = "less than " + steps[0]; //$NON-NLS-1$
bucket = "less than " + steps[0];
}
// if greater than largest value
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
{
@ -664,6 +664,20 @@ public final class LocalyticsSession
*/
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
*/
@ -952,12 +966,11 @@ public final class LocalyticsSession
return;
}
/*
* 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
* data but the server won't ever receive the opt-out message which will cause data to be deleted. This is not
* expected to be likely, and generally still meets user expectations because no new data will be uploaded.
*/
mProvider.runBatchTransaction(new Runnable()
{
@Override
public void run()
{
final ContentValues values = new ContentValues();
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$
@ -975,6 +988,8 @@ public final class LocalyticsSession
{
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
@ -989,13 +1004,13 @@ public final class LocalyticsSession
* <p>
* This method must only be called after {@link #init()} is called.
* <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public
* interface is to send {@link #MESSAGE_OPEN} to the Handler.
* Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* 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.
* @see #MESSAGE_OPEN
*/
public void open(final boolean ignoreLimits)
/* package */void open(final boolean ignoreLimits)
{
if (mIsSessionOpen)
{
@ -1017,40 +1032,8 @@ public final class LocalyticsSession
}
/*
* 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;
}
}
}
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
* 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.
*/
long closeEventId = -1; // sentinel value
@ -1061,11 +1044,9 @@ public final class LocalyticsSession
try
{
eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
{
EventsDbColumns._ID,
EventsDbColumns.WALL_TIME }, String.format("%s = ?", EventsDbColumns.EVENT_NAME), new String[] { CLOSE_EVENT }, EventsDbColumns._ID); //$NON-NLS-1$
{ 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$
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 CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[]
@ -1078,9 +1059,8 @@ public final class LocalyticsSession
{
case LEFT:
{
if (System.currentTimeMillis() - eventsCursor.getLong(eventsCursor.getColumnIndexOrThrow(EventsDbColumns.WALL_TIME)) < Constants.SESSION_EXPIRATION)
{
if (closeEventId != -1)
if (-1 != closeEventId)
{
/*
* 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$
}
long newClose = eventsCursor.getLong(eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID));
if (newClose > closeEventId)
{
closeEventId = newClose;
}
}
if (-1 == closeEventId)
{
closeEventId = eventsCursor.getLong(idColumn);
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$
openNewSession();
Log.v(Constants.LOG_TAG, "Opening old closed session and reconnecting"); //$NON-NLS-1$
mIsSessionOpen = true;
openClosedSession(closeEventId);
}
else
{
Log.v(Constants.LOG_TAG, "Opening old session and reconnecting"); //$NON-NLS-1$
openOldSession(closeEventId);
Cursor sessionsCursor = null;
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()
{
// first insert the session
{
final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
final ContentValues values = new ContentValues();
@ -1191,15 +1263,21 @@ public final class LocalyticsSession
values.put(SessionsDbColumns.NETWORK_COUNTRY, telephonyManager.getNetworkCountryIso());
values.put(SessionsDbColumns.NETWORK_TYPE, DatapointHelper.getNetworkType(mContext, telephonyManager));
mProvider.runBatchTransaction(new Runnable()
{
@Override
public void run()
{
mSessionId = mProvider.insert(SessionsDbColumns.TABLE_NAME, values);
if (mSessionId == -1)
{
throw new RuntimeException("session insert failed"); //$NON-NLS-1$
}
}
tagEvent(OPEN_EVENT, null);
}
});
/*
* 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
* last close event
*/
private void openOldSession(final long closeEventId)
private void openClosedSession(final long closeEventId)
{
// reconnect old session
Cursor eventCursor = null;
try
{
@ -1259,14 +1335,14 @@ public final class LocalyticsSession
* <p>
* This method must only be called after {@link #init()} is called.
* <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public
* interface is to send {@link #MESSAGE_CLOSE} to the Handler.
* Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* public interface is to send {@link #MESSAGE_CLOSE} to the Handler.
*
* @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)
{
@ -1286,15 +1362,15 @@ public final class LocalyticsSession
* <p>
* This method must only be called after {@link #init()} is called.
* <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public
* interface is to send {@link #MESSAGE_TAG_EVENT} to the Handler.
* Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* public interface is to send {@link #MESSAGE_TAG_EVENT} to the Handler.
*
* @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
* method has the same effect as calling {@link #tagEvent(String)}.
* @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)
{
@ -1442,8 +1518,6 @@ public final class LocalyticsSession
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$
if (cursor.moveToFirst())
{
if (cursor.moveToFirst())
{
if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME))))
@ -1456,7 +1530,6 @@ public final class LocalyticsSession
}
}
}
}
finally
{
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.
*/
/* 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
@ -1496,10 +1569,11 @@ public final class LocalyticsSession
try
{
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[]
{ 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[]
{ 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
* 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
@ -1568,11 +1642,10 @@ public final class LocalyticsSession
{
EventsDbColumns._ID,
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[]
{ 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 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
* occur.
* <p>
* Note: This method is a private implementation detail. It is only made public for unit testing purposes. The public
* interface is to send {@link #MESSAGE_UPLOAD} to the Handler.
* Note: This method is a private implementation detail. It is only made package accessible for unit testing purposes. The
* 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.
* @see #MESSAGE_UPLOAD
*/
public void upload(final Runnable callback)
/* package */void upload(final Runnable callback)
{
if (sIsUploadingMap.get(mApiKey).booleanValue())
{
@ -1671,7 +1744,6 @@ public final class LocalyticsSession
{
mProvider.runBatchTransaction(new Runnable()
{
@Override
public void run()
{
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
* 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
{
/**
* 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.
*/
@ -1800,14 +1877,13 @@ public final class LocalyticsSession
for (final JSONObject json : toUpload)
{
builder.append(json.toString());
builder.append("\n"); //$NON-NLS-1$
builder.append('\n');
}
if (uploadSessions(String.format(ANALYTICS_URL, mApiKey), builder.toString()))
{
mProvider.runBatchTransaction(new Runnable()
{
@Override
public void run()
{
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
* library
*/
new Thread(callback).start();
new Thread(callback, UPLOAD_CALLBACK_THREAD_NAME).start();
}
mSessionHandler.sendEmptyMessage(SessionHandler.MESSAGE_UPLOAD_COMPLETE);
@ -1948,7 +2024,10 @@ public final class LocalyticsSession
}
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)
{ //
{
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
* @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)
{
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
* @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)
{
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
* @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)
{
return (T) helper(target, null, null, methodName, types, args);
return helper(target, null, null, methodName, types, args);
}
@SuppressWarnings("unchecked")

@ -26,8 +26,10 @@ public class StatisticsService {
if(dontCollectStatistics())
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;
}
localyticsSession = new LocalyticsSession(context.getApplicationContext(),
Constants.LOCALYTICS_KEY);
@ -55,9 +57,10 @@ public class StatisticsService {
if(dontCollectStatistics())
return;
if(localyticsSession != null)
if(localyticsSession != null) {
localyticsSession.close();
}
}
/**
* Indicates an error occurred

Loading…
Cancel
Save