Merge pull request #77 from sbosley/110829_sb_localytics_fixes

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

@ -16,12 +16,12 @@ import android.text.format.DateUtils;
//@formatter:off
/*
* Version history:
*
*
* 1.6: Fixed network type reporting. Added reporting of app signature, device SDK level, device manufacturer, serial number.
* 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,11 +72,16 @@ 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();
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private Constants()

@ -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,28 +54,40 @@ 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);
}
catch (Exception e)
{
// Although probably not necessary, protects from the aforementioned deprecation
try
{
Class<?> buildClass = Build.VERSION.class;
return buildClass.getField("SDK_INT").getInt(null); // $NON-NLS-1$
}
catch (Exception ignore) { /**/ }
}
return 3;
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.
final Class<?> buildClass = Build.VERSION.class;
final String sdkString = (String) buildClass.getField("SDK").get(null); //$NON-NLS-1$
return Integer.parseInt(sdkString);
}
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
{
final Class<?> buildClass = Build.VERSION.class;
return buildClass.getField("SDK_INT").getInt(null); //$NON-NLS-1$
}
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$
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$
}
}
final String id = getTelephonyDeviceIdOrNull(context);
if (id == null)
if (null == id)
{
return null;
}
@ -306,21 +294,28 @@ 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
*/
public static String getManufacturer()
{
String mfg = "unknown"; // $NON-NLS-1$
String mfg = "unknown"; //$NON-NLS-1$
if (Constants.CURRENT_API_LEVEL > 3)
{
try
{
Class<?> buildClass = Build.class;
mfg = (String) buildClass.getField("MANUFACTURER").get(null); // $NON-NLS-1$
final Class<?> buildClass = Build.class;
mfg = (String) buildClass.getField("MANUFACTURER").get(null); //$NON-NLS-1$
}
catch (final Exception ignore)
{
if (Constants.IS_LOGGABLE)
{
Log.w(Constants.LOG_TAG, "Caught exception", ignore); //$NON-NLS-1$
}
}
catch (Exception ignore) {}
}
return mfg;
}

@ -11,7 +11,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private JsonObjects()
@ -26,7 +26,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private BlobHeader()
@ -36,7 +36,7 @@ import org.json.JSONArray;
/**
* Data type for the JSON object.
*
*
* @see #VALUE_DATA_TYPE
*/
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
@ -75,7 +75,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private Attributes()
@ -155,8 +155,8 @@ import org.json.JSONArray;
* Type: {@code int}
* <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$
@ -193,7 +193,7 @@ import org.json.JSONArray;
* Type: {@code String}
* <p>
* Localytics library version
*
*
* @see Constants#LOCALYTICS_CLIENT_LIBRARY_VERSION
*/
public static final String KEY_LOCALYTICS_CLIENT_LIBRARY_VERSION = "lv"; //$NON-NLS-1$
@ -202,7 +202,7 @@ import org.json.JSONArray;
* Type: {@code String}
* <p>
* Data type for the JSON object.
*
*
* @see #VALUE_DATA_TYPE
*/
public static final String KEY_LOCALYTICS_DATA_TYPE = "dt"; //$NON-NLS-1$
@ -228,7 +228,7 @@ import org.json.JSONArray;
/**
* Value for the platform.
*
*
* @see #KEY_DEVICE_PLATFORM
*/
public static final String VALUE_PLATFORM = "Android"; //$NON-NLS-1$
@ -242,7 +242,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private SessionOpen()
@ -254,7 +254,7 @@ import org.json.JSONArray;
* Type: {@code String}
* <p>
* Data type for the JSON object.
*
*
* @see #VALUE_DATA_TYPE
*/
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
@ -293,7 +293,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private SessionClose()
@ -305,7 +305,7 @@ import org.json.JSONArray;
* Type: {@code String}
* <p>
* Data type for the JSON object.
*
*
* @see #VALUE_DATA_TYPE
*/
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
@ -354,7 +354,7 @@ import org.json.JSONArray;
/**
* Data type for close events.
*
*
* @see #KEY_DATA_TYPE
*/
public static final String VALUE_DATA_TYPE = "c"; //$NON-NLS-1$
@ -367,7 +367,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private SessionEvent()
@ -379,14 +379,14 @@ import org.json.JSONArray;
* Type: {@code String}
* <p>
* Data type for the JSON object.
*
*
* @see #VALUE_DATA_TYPE
*/
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
/**
* Data type for application events.
*
*
* @see #KEY_DATA_TYPE
*/
public static final String VALUE_DATA_TYPE = "e"; //$NON-NLS-1$
@ -437,7 +437,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private OptEvent()
@ -449,14 +449,14 @@ import org.json.JSONArray;
* Type: {@code String}
* <p>
* Data type for the JSON object.
*
*
* @see #VALUE_DATA_TYPE
*/
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
/**
* Data type for opt in/out events.
*
*
* @see #KEY_DATA_TYPE
*/
public static final String VALUE_DATA_TYPE = "o"; //$NON-NLS-1$
@ -490,7 +490,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private EventFlow()
@ -502,7 +502,7 @@ import org.json.JSONArray;
* Type: {@code String}
* <p>
* Data type for the JSON object.
*
*
* @see #VALUE_DATA_TYPE
*/
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
@ -547,7 +547,7 @@ import org.json.JSONArray;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private Element()

@ -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)}.
@ -76,13 +77,13 @@ import java.util.Set;
* <p>
* Note: if {@code context} is an instance of {@link android.test.RenamingDelegatingContext}, then a new object will be
* returned every time. This is not a "public" API, but is documented here as it aids unit testing.
*
*
* @param context Application context. Cannot be null.
* @param apiKey TODO
* @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
@ -124,7 +125,7 @@ import java.util.Set;
* Constructs a new Localytics Provider.
* <p>
* Note: this method may perform disk operations.
*
*
* @param context application context. Cannot be null.
*/
private LocalyticsProvider(final Context context, final String apiKey)
@ -143,7 +144,7 @@ import java.util.Set;
* Inserts a new record.
* <p>
* Note: this method may perform disk operations.
*
*
* @param tableName name of the table operate on. Must be one of the recognized tables. Cannot be null.
* @param values ContentValues to insert. Cannot be null.
* @return the {@link BaseColumns#_ID} of the inserted row or -1 if an error occurred.
@ -184,7 +185,7 @@ import java.util.Set;
* Performs a query.
* <p>
* Note: this method may perform disk operations.
*
*
* @param tableName name of the table operate on. Must be one of the recognized tables. Cannot be null.
* @param projection The list of columns to include. If null, then all columns are included by default.
* @param selection A filter to apply to all rows, like the SQLite WHERE clause. Passing null will query all rows. This param
@ -226,7 +227,7 @@ import java.util.Set;
* Updates row(s).
* <p>
* Note: this method may perform disk operations.
*
*
* @param tableName name of the table operate on. Must be one of the recognized tables. Cannot be null.
* @param values A ContentValues mapping from column names (see the associated BaseColumns class for the table) to new column
* values.
@ -258,7 +259,7 @@ import java.util.Set;
* Deletes row(s).
* <p>
* Note: this method may perform disk operations.
*
*
* @param tableName name of the table operate on. Must be one of the recognized tables. Cannot be null.
* @param selection A filter to limit which rows are deleted, like the SQLite WHERE clause. Passing null implies all rows.
* This param may contain ? symbols, which will be replaced by values from the {@code selectionArgs} param.
@ -301,14 +302,14 @@ import java.util.Set;
/**
* Executes an arbitrary runnable with exclusive access to the database, essentially allowing an atomic transaction.
*
*
* @param runnable Runnable to execute. Cannot be null.
* @throws IllegalArgumentException if {@code runnable} is null
*/
/*
* This implementation is sort of a hack. In the future, it would be better model this after applyBatch() with a list of
* ContentProviderOperation objects. But that API isn't available until Android 2.0.
*
*
* An alternative implementation would have been to expose the begin/end transaction methods on the Provider object. While
* that would work, it makes it harder to transition to a ContentProviderOperation model in the future.
*/
@ -337,7 +338,7 @@ import java.util.Set;
/**
* Private helper to test whether a given table name is valid
*
*
* @param table name of a table to check. This param may be null.
* @return true if the table is valid, false if the table is invalid. If {@code table} is null, returns false.
*/
@ -358,7 +359,7 @@ import java.util.Set;
/**
* Private helper that knows all the tables that {@link LocalyticsProvider} can operate on.
*
*
* @return returns a set of the valid tables.
*/
private static Set<String> getValidTables()
@ -380,7 +381,7 @@ import java.util.Set;
* Private helper that deletes files from older versions of the Localytics library.
* <p>
* Note: This is a private method that is only made package-accessible for unit testing.
*
*
* @param context application context
* @throws IllegalArgumentException if {@code context} is null
*/
@ -399,7 +400,7 @@ import java.util.Set;
/**
* Private helper to delete a directory, regardless of whether the directory is empty.
*
*
* @param directory Directory or file to delete. Cannot be null.
* @return true if deletion was successful. False if deletion failed.
*/
@ -452,7 +453,7 @@ import java.util.Set;
* <p>
* If an error occurs during initialization and an exception is thrown, {@link SQLiteDatabase#close()} will not be called
* by this method. That responsibility is left to the caller.
*
*
* @param db The database to perform post-creation processing on. db cannot not be null
* @throws IllegalArgumentException if db is null
*/
@ -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,11 +583,11 @@ 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
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private ApiKeysDbColumns()
@ -606,7 +648,7 @@ import java.util.Set;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private AttributesDbColumns()
@ -658,7 +700,7 @@ import java.util.Set;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private EventsDbColumns()
@ -728,7 +770,7 @@ import java.util.Set;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private EventHistoryDbColumns()
@ -797,7 +839,7 @@ import java.util.Set;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private SessionsDbColumns()
@ -845,7 +887,7 @@ import java.util.Set;
* TYPE: {@code String}
* <p>
* Version of the Localytics client library.
*
*
* @see Constants#LOCALYTICS_CLIENT_LIBRARY_VERSION
*/
public static final String LOCALYTICS_LIBRARY_VERSION = "localytics_library_version"; //$NON-NLS-1$
@ -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$
@ -885,7 +927,7 @@ import java.util.Set;
* String representing the device model
* <p>
* Constraints: None
*
*
* @see android.os.Build#MODEL
*/
public static final String DEVICE_MODEL = "device_model"; //$NON-NLS-1$
@ -896,7 +938,7 @@ import java.util.Set;
* String representing the device manufacturer
* <p>
* Constraints: None
*
*
* @see android.os.Build#MANUFACTURER
*/
public static final String DEVICE_MANUFACTURER = "device_manufacturer"; //$NON-NLS-1$
@ -907,7 +949,7 @@ import java.util.Set;
* String representing a hash of the device Android ID
* <p>
* Constraints: None
*
*
* @see android.provider.Settings.Secure#ANDROID_ID
*/
public static final String DEVICE_ANDROID_ID_HASH = "device_android_id_hash"; //$NON-NLS-1$
@ -919,7 +961,7 @@ import java.util.Set;
* parent application doesn't have {@link android.Manifest.permission#READ_PHONE_STATE}.
* <p>
* Constraints: None
*
*
* @see android.telephony.TelephonyManager#getDeviceId()
*/
public static final String DEVICE_TELEPHONY_ID = "device_telephony_id"; //$NON-NLS-1$
@ -931,7 +973,7 @@ import java.util.Set;
* if the parent application doesn't have {@link android.Manifest.permission#READ_PHONE_STATE}.
* <p>
* Constraints: None
*
*
* @see android.telephony.TelephonyManager#getDeviceId()
*/
public static final String DEVICE_TELEPHONY_ID_HASH = "device_telephony_id_hash"; //$NON-NLS-1$
@ -997,7 +1039,7 @@ import java.util.Set;
* networks, Ethernet, etc.
* <p>
* Constraints: None
*
*
* @see android.telephony.TelephonyManager
*/
public static final String NETWORK_TYPE = "network_type"; //$NON-NLS-1$
@ -1033,7 +1075,7 @@ import java.util.Set;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private UploadBlobEventsDbColumns()
@ -1076,7 +1118,7 @@ import java.util.Set;
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private UploadBlobsDbColumns()

@ -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,29 +966,30 @@ 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.
*/
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$
if (!mIsSessionOpen)
{
/*
* Force a session to contain the opt event
*/
open(true);
tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null);
close();
}
else
mProvider.runBatchTransaction(new Runnable()
{
tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null);
}
@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$
if (!mIsSessionOpen)
{
/*
* Force a session to contain the opt event
*/
open(true);
tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null);
close();
}
else
{
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,20 +1059,27 @@ public final class LocalyticsSession
{
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
*/
if (Constants.IS_LOGGABLE)
{
/*
* This should never happen
*/
if (Constants.IS_LOGGABLE)
{
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);
break;
}
break;
@ -1119,54 +1107,91 @@ 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);
/**
* Opens a new session. This is a helper method to {@link #open(boolean)}.
*
* @effects Updates the database by creating a new entry in the {@link SessionsDbColumns} table.
*/
private void openNewSession()
{
// first insert the session
{
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;
}
final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
// 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);
final ContentValues values = new ContentValues();
values.put(SessionsDbColumns.API_KEY_REF, Long.valueOf(mApiKeyId));
values.put(SessionsDbColumns.SESSION_START_WALL_TIME, Long.valueOf(System.currentTimeMillis()));
values.put(SessionsDbColumns.UUID, UUID.randomUUID().toString());
values.put(SessionsDbColumns.APP_VERSION, DatapointHelper.getAppVersion(mContext));
values.put(SessionsDbColumns.ANDROID_SDK, Integer.valueOf(Constants.CURRENT_API_LEVEL));
values.put(SessionsDbColumns.ANDROID_VERSION, VERSION.RELEASE);
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;
}
}
// Try and get the deviceId. If it is unavailable (or invalid) use the installation ID instead.
String deviceId = DatapointHelper.getAndroidIdHashOrNull(mContext);
if (deviceId == null)
/*
* Check that the maximum number of sessions hasn't been exceeded
*/
if (!ignoreLimits)
{
Cursor cursor = null;
try
{
cursor = mProvider.query(ApiKeysDbColumns.TABLE_NAME, null, String.format("%s = ?", ApiKeysDbColumns.API_KEY), new String[] { mApiKey }, null); //$NON-NLS-1$
if (cursor.moveToFirst())
cursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[]
{ SessionsDbColumns._ID }, null, null, null);
if (cursor.getCount() >= Constants.MAX_NUM_SESSIONS)
{
deviceId = cursor.getString(cursor.getColumnIndexOrThrow(ApiKeysDbColumns.UUID));
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 (null != cursor)
if (cursor != null)
{
cursor.close();
cursor = null;
@ -1174,32 +1199,85 @@ public final class LocalyticsSession
}
}
values.put(SessionsDbColumns.DEVICE_ANDROID_ID_HASH, deviceId);
values.put(SessionsDbColumns.DEVICE_COUNTRY, telephonyManager.getSimCountryIso());
values.put(SessionsDbColumns.DEVICE_MANUFACTURER, DatapointHelper.getManufacturer());
values.put(SessionsDbColumns.DEVICE_MODEL, Build.MODEL);
values.put(SessionsDbColumns.DEVICE_SERIAL_NUMBER_HASH, DatapointHelper.getSerialNumberHashOrNull());
values.put(SessionsDbColumns.DEVICE_TELEPHONY_ID, DatapointHelper.getTelephonyDeviceIdOrNull(mContext));
values.put(SessionsDbColumns.DEVICE_TELEPHONY_ID_HASH, DatapointHelper.getTelephonyDeviceIdHashOrNull(mContext));
values.put(SessionsDbColumns.LOCALE_COUNTRY, Locale.getDefault().getCountry());
values.put(SessionsDbColumns.LOCALE_LANGUAGE, Locale.getDefault().getLanguage());
values.put(SessionsDbColumns.LOCALYTICS_LIBRARY_VERSION, Constants.LOCALYTICS_CLIENT_LIBRARY_VERSION);
Log.v(Constants.LOG_TAG, "Opening new session"); //$NON-NLS-1$
mIsSessionOpen = true;
values.putNull(SessionsDbColumns.LATITUDE);
values.putNull(SessionsDbColumns.LONGITUDE);
values.put(SessionsDbColumns.NETWORK_CARRIER, telephonyManager.getNetworkOperatorName());
values.put(SessionsDbColumns.NETWORK_COUNTRY, telephonyManager.getNetworkCountryIso());
values.put(SessionsDbColumns.NETWORK_TYPE, DatapointHelper.getNetworkType(mContext, telephonyManager));
openNewSession();
}
}
/**
* Opens a new session. This is a helper method to {@link #open(boolean)}.
*
* @effects Updates the database by creating a new entry in the {@link SessionsDbColumns} table.
*/
private void openNewSession()
{
final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mSessionId = mProvider.insert(SessionsDbColumns.TABLE_NAME, values);
final ContentValues values = new ContentValues();
values.put(SessionsDbColumns.API_KEY_REF, Long.valueOf(mApiKeyId));
values.put(SessionsDbColumns.SESSION_START_WALL_TIME, Long.valueOf(System.currentTimeMillis()));
values.put(SessionsDbColumns.UUID, UUID.randomUUID().toString());
values.put(SessionsDbColumns.APP_VERSION, DatapointHelper.getAppVersion(mContext));
values.put(SessionsDbColumns.ANDROID_SDK, Integer.valueOf(Constants.CURRENT_API_LEVEL));
values.put(SessionsDbColumns.ANDROID_VERSION, VERSION.RELEASE);
if (mSessionId == -1)
// Try and get the deviceId. If it is unavailable (or invalid) use the installation ID instead.
String deviceId = DatapointHelper.getAndroidIdHashOrNull(mContext);
if (deviceId == null)
{
Cursor cursor = null;
try
{
cursor = mProvider.query(ApiKeysDbColumns.TABLE_NAME, null, String.format("%s = ?", ApiKeysDbColumns.API_KEY), new String[] { mApiKey }, null); //$NON-NLS-1$
if (cursor.moveToFirst())
{
deviceId = cursor.getString(cursor.getColumnIndexOrThrow(ApiKeysDbColumns.UUID));
}
}
finally
{
throw new RuntimeException("session insert failed"); //$NON-NLS-1$
if (null != cursor)
{
cursor.close();
cursor = null;
}
}
}
tagEvent(OPEN_EVENT, null);
values.put(SessionsDbColumns.DEVICE_ANDROID_ID_HASH, deviceId);
values.put(SessionsDbColumns.DEVICE_COUNTRY, telephonyManager.getSimCountryIso());
values.put(SessionsDbColumns.DEVICE_MANUFACTURER, DatapointHelper.getManufacturer());
values.put(SessionsDbColumns.DEVICE_MODEL, Build.MODEL);
values.put(SessionsDbColumns.DEVICE_SERIAL_NUMBER_HASH, DatapointHelper.getSerialNumberHashOrNull());
values.put(SessionsDbColumns.DEVICE_TELEPHONY_ID, DatapointHelper.getTelephonyDeviceIdOrNull(mContext));
values.put(SessionsDbColumns.DEVICE_TELEPHONY_ID_HASH, DatapointHelper.getTelephonyDeviceIdHashOrNull(mContext));
values.put(SessionsDbColumns.LOCALE_COUNTRY, Locale.getDefault().getCountry());
values.put(SessionsDbColumns.LOCALE_LANGUAGE, Locale.getDefault().getLanguage());
values.put(SessionsDbColumns.LOCALYTICS_LIBRARY_VERSION, Constants.LOCALYTICS_CLIENT_LIBRARY_VERSION);
values.putNull(SessionsDbColumns.LATITUDE);
values.putNull(SessionsDbColumns.LONGITUDE);
values.put(SessionsDbColumns.NETWORK_CARRIER, telephonyManager.getNetworkOperatorName());
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)
{
@ -1444,16 +1520,13 @@ public final class LocalyticsSession
if (cursor.moveToFirst())
{
if (cursor.moveToFirst())
if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME))))
{
if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME))))
if (Constants.IS_LOGGABLE)
{
if (Constants.IS_LOGGABLE)
{
Log.v(Constants.LOG_TAG, String.format("Suppressed duplicate screen %s", screen)); //$NON-NLS-1$
}
return;
Log.v(Constants.LOG_TAG, String.format("Suppressed duplicate screen %s", screen)); //$NON-NLS-1$
}
return;
}
}
}
@ -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$
}
}
}
}

@ -12,7 +12,7 @@ public final class ReflectionUtils
{
/**
* Private constructor prevents instantiation
*
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private ReflectionUtils()
@ -22,7 +22,7 @@ public final class ReflectionUtils
/**
* Use reflection to invoke a static method for a class object and method name
*
*
* @param <T> Type that the method should return
* @param classObject Class on which to invoke {@code methodName}. Cannot be null.
* @param methodName Name of the method to invoke. Cannot be null.
@ -31,15 +31,14 @@ 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);
}
/**
* Use reflection to invoke a static method for a class object and method name
*
*
* @param <T> Type that the method should return
* @param className Name of the class on which to invoke {@code methodName}. Cannot be null.
* @param methodName Name of the method to invoke. Cannot be null.
@ -48,15 +47,14 @@ 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);
}
/**
* Use reflection to invoke a static method for a class object and method name
*
*
* @param <T> Type that the method should return
* @param target Object instance on which to invoke {@code methodName}. Cannot be null.
* @param methodName Name of the method to invoke. Cannot be null.
@ -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,8 +57,9 @@ public class StatisticsService {
if(dontCollectStatistics())
return;
if(localyticsSession != null)
if(localyticsSession != null) {
localyticsSession.close();
}
}
/**

Loading…
Cancel
Save