Merge pull request #77 from sbosley/110829_sb_localytics_fixes

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

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

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

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

@ -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)}.
@ -76,13 +77,13 @@ import java.util.Set;
* <p> * <p>
* Note: if {@code context} is an instance of {@link android.test.RenamingDelegatingContext}, then a new object will be * 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. * 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 context Application context. Cannot be null.
* @param apiKey TODO * @param apiKey TODO
* @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
@ -124,7 +125,7 @@ import java.util.Set;
* Constructs a new Localytics Provider. * Constructs a new Localytics Provider.
* <p> * <p>
* Note: this method may perform disk operations. * Note: this method may perform disk operations.
* *
* @param context application context. Cannot be null. * @param context application context. Cannot be null.
*/ */
private LocalyticsProvider(final Context context, final String apiKey) private LocalyticsProvider(final Context context, final String apiKey)
@ -143,7 +144,7 @@ import java.util.Set;
* Inserts a new record. * Inserts a new record.
* <p> * <p>
* Note: this method may perform disk operations. * 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 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. * @param values ContentValues to insert. Cannot be null.
* @return the {@link BaseColumns#_ID} of the inserted row or -1 if an error occurred. * @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. * Performs a query.
* <p> * <p>
* Note: this method may perform disk operations. * 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 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 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 * @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). * Updates row(s).
* <p> * <p>
* Note: this method may perform disk operations. * 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 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 * @param values A ContentValues mapping from column names (see the associated BaseColumns class for the table) to new column
* values. * values.
@ -258,7 +259,7 @@ import java.util.Set;
* Deletes row(s). * Deletes row(s).
* <p> * <p>
* Note: this method may perform disk operations. * 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 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. * @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. * 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. * Executes an arbitrary runnable with exclusive access to the database, essentially allowing an atomic transaction.
* *
* @param runnable Runnable to execute. Cannot be null. * @param runnable Runnable to execute. Cannot be null.
* @throws IllegalArgumentException if {@code runnable} is 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 * 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. * 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 * 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. * 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 * Private helper to test whether a given table name is valid
* *
* @param table name of a table to check. This param may be null. * @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. * @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. * Private helper that knows all the tables that {@link LocalyticsProvider} can operate on.
* *
* @return returns a set of the valid tables. * @return returns a set of the valid tables.
*/ */
private static Set<String> getValidTables() 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. * Private helper that deletes files from older versions of the Localytics library.
* <p> * <p>
* Note: This is a private method that is only made package-accessible for unit testing. * Note: This is a private method that is only made package-accessible for unit testing.
* *
* @param context application context * @param context application context
* @throws IllegalArgumentException if {@code context} is null * @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. * Private helper to delete a directory, regardless of whether the directory is empty.
* *
* @param directory Directory or file to delete. Cannot be null. * @param directory Directory or file to delete. Cannot be null.
* @return true if deletion was successful. False if deletion failed. * @return true if deletion was successful. False if deletion failed.
*/ */
@ -452,7 +453,7 @@ import java.util.Set;
* <p> * <p>
* If an error occurs during initialization and an exception is thrown, {@link SQLiteDatabase#close()} will not be called * 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. * 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 * @param db The database to perform post-creation processing on. db cannot not be null
* @throws IllegalArgumentException if db is null * @throws IllegalArgumentException if db is null
*/ */
@ -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,11 +583,11 @@ 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
* *
* @throws UnsupportedOperationException because this class cannot be instantiated. * @throws UnsupportedOperationException because this class cannot be instantiated.
*/ */
private ApiKeysDbColumns() private ApiKeysDbColumns()
@ -606,7 +648,7 @@ import java.util.Set;
{ {
/** /**
* Private constructor prevents instantiation * Private constructor prevents instantiation
* *
* @throws UnsupportedOperationException because this class cannot be instantiated. * @throws UnsupportedOperationException because this class cannot be instantiated.
*/ */
private AttributesDbColumns() private AttributesDbColumns()
@ -658,7 +700,7 @@ import java.util.Set;
{ {
/** /**
* Private constructor prevents instantiation * Private constructor prevents instantiation
* *
* @throws UnsupportedOperationException because this class cannot be instantiated. * @throws UnsupportedOperationException because this class cannot be instantiated.
*/ */
private EventsDbColumns() private EventsDbColumns()
@ -728,7 +770,7 @@ import java.util.Set;
{ {
/** /**
* Private constructor prevents instantiation * Private constructor prevents instantiation
* *
* @throws UnsupportedOperationException because this class cannot be instantiated. * @throws UnsupportedOperationException because this class cannot be instantiated.
*/ */
private EventHistoryDbColumns() private EventHistoryDbColumns()
@ -797,7 +839,7 @@ import java.util.Set;
{ {
/** /**
* Private constructor prevents instantiation * Private constructor prevents instantiation
* *
* @throws UnsupportedOperationException because this class cannot be instantiated. * @throws UnsupportedOperationException because this class cannot be instantiated.
*/ */
private SessionsDbColumns() private SessionsDbColumns()
@ -845,7 +887,7 @@ import java.util.Set;
* TYPE: {@code String} * TYPE: {@code String}
* <p> * <p>
* Version of the Localytics client library. * Version of the Localytics client library.
* *
* @see Constants#LOCALYTICS_CLIENT_LIBRARY_VERSION * @see Constants#LOCALYTICS_CLIENT_LIBRARY_VERSION
*/ */
public static final String LOCALYTICS_LIBRARY_VERSION = "localytics_library_version"; //$NON-NLS-1$ public static final String LOCALYTICS_LIBRARY_VERSION = "localytics_library_version"; //$NON-NLS-1$
@ -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$
@ -885,7 +927,7 @@ import java.util.Set;
* String representing the device model * String representing the device model
* <p> * <p>
* Constraints: None * Constraints: None
* *
* @see android.os.Build#MODEL * @see android.os.Build#MODEL
*/ */
public static final String DEVICE_MODEL = "device_model"; //$NON-NLS-1$ public static final String DEVICE_MODEL = "device_model"; //$NON-NLS-1$
@ -896,7 +938,7 @@ import java.util.Set;
* String representing the device manufacturer * String representing the device manufacturer
* <p> * <p>
* Constraints: None * Constraints: None
* *
* @see android.os.Build#MANUFACTURER * @see android.os.Build#MANUFACTURER
*/ */
public static final String DEVICE_MANUFACTURER = "device_manufacturer"; //$NON-NLS-1$ 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 * String representing a hash of the device Android ID
* <p> * <p>
* Constraints: None * Constraints: None
* *
* @see android.provider.Settings.Secure#ANDROID_ID * @see android.provider.Settings.Secure#ANDROID_ID
*/ */
public static final String DEVICE_ANDROID_ID_HASH = "device_android_id_hash"; //$NON-NLS-1$ 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}. * parent application doesn't have {@link android.Manifest.permission#READ_PHONE_STATE}.
* <p> * <p>
* Constraints: None * Constraints: None
* *
* @see android.telephony.TelephonyManager#getDeviceId() * @see android.telephony.TelephonyManager#getDeviceId()
*/ */
public static final String DEVICE_TELEPHONY_ID = "device_telephony_id"; //$NON-NLS-1$ 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}. * if the parent application doesn't have {@link android.Manifest.permission#READ_PHONE_STATE}.
* <p> * <p>
* Constraints: None * Constraints: None
* *
* @see android.telephony.TelephonyManager#getDeviceId() * @see android.telephony.TelephonyManager#getDeviceId()
*/ */
public static final String DEVICE_TELEPHONY_ID_HASH = "device_telephony_id_hash"; //$NON-NLS-1$ 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. * networks, Ethernet, etc.
* <p> * <p>
* Constraints: None * Constraints: None
* *
* @see android.telephony.TelephonyManager * @see android.telephony.TelephonyManager
*/ */
public static final String NETWORK_TYPE = "network_type"; //$NON-NLS-1$ public static final String NETWORK_TYPE = "network_type"; //$NON-NLS-1$
@ -1033,7 +1075,7 @@ import java.util.Set;
{ {
/** /**
* Private constructor prevents instantiation * Private constructor prevents instantiation
* *
* @throws UnsupportedOperationException because this class cannot be instantiated. * @throws UnsupportedOperationException because this class cannot be instantiated.
*/ */
private UploadBlobEventsDbColumns() private UploadBlobEventsDbColumns()
@ -1076,7 +1118,7 @@ import java.util.Set;
{ {
/** /**
* Private constructor prevents instantiation * Private constructor prevents instantiation
* *
* @throws UnsupportedOperationException because this class cannot be instantiated. * @throws UnsupportedOperationException because this class cannot be instantiated.
*/ */
private UploadBlobsDbColumns() private UploadBlobsDbColumns()

@ -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,29 +966,30 @@ 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
* 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
{ {
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 * 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,20 +1059,27 @@ 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
*/
if (Constants.IS_LOGGABLE)
{ {
/* Log.w(Constants.LOG_TAG, "There were multiple close events within SESSION_EXPIRATION"); //$NON-NLS-1$
* This should never happen
*/
if (Constants.IS_LOGGABLE)
{
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,54 +1107,91 @@ 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())
* Opens a new session. This is a helper method to {@link #open(boolean)}. {
* if (sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns.SESSION_START_WALL_TIME)) >= System.currentTimeMillis()
* @effects Updates the database by creating a new entry in the {@link SessionsDbColumns} table. - Constants.SESSION_EXPIRATION)
*/ {
private void openNewSession() // reconnect
{ Log.v(Constants.LOG_TAG, "Opening old unclosed session and reconnecting"); //$NON-NLS-1$
// first insert the session 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(); if (eventsCursor.getCount() == 0)
values.put(SessionsDbColumns.API_KEY_REF, Long.valueOf(mApiKeyId)); {
values.put(SessionsDbColumns.SESSION_START_WALL_TIME, Long.valueOf(System.currentTimeMillis())); mProvider.delete(SessionsDbColumns.TABLE_NAME, String.format("%s = ?", SessionsDbColumns._ID), new String[] { sessionId }); //$NON-NLS-1$
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)); finally
values.put(SessionsDbColumns.ANDROID_VERSION, VERSION.RELEASE); {
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); * Check that the maximum number of sessions hasn't been exceeded
if (deviceId == null) */
if (!ignoreLimits)
{ {
Cursor cursor = null; Cursor cursor = null;
try try
{ {
cursor = mProvider.query(ApiKeysDbColumns.TABLE_NAME, null, String.format("%s = ?", ApiKeysDbColumns.API_KEY), new String[] { mApiKey }, null); //$NON-NLS-1$ cursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[]
if (cursor.moveToFirst()) { 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 finally
{ {
if (null != cursor) if (cursor != null)
{ {
cursor.close(); cursor.close();
cursor = null; cursor = null;
@ -1174,32 +1199,85 @@ public final class LocalyticsSession
} }
} }
values.put(SessionsDbColumns.DEVICE_ANDROID_ID_HASH, deviceId); Log.v(Constants.LOG_TAG, "Opening new session"); //$NON-NLS-1$
values.put(SessionsDbColumns.DEVICE_COUNTRY, telephonyManager.getSimCountryIso()); mIsSessionOpen = true;
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); openNewSession();
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)); /**
* 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. * 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)
{ {
@ -1444,16 +1520,13 @@ public final class LocalyticsSession
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)))) if (Constants.IS_LOGGABLE)
{ {
if (Constants.IS_LOGGABLE) Log.v(Constants.LOG_TAG, String.format("Suppressed duplicate screen %s", screen)); //$NON-NLS-1$
{
Log.v(Constants.LOG_TAG, String.format("Suppressed duplicate screen %s", screen)); //$NON-NLS-1$
}
return;
} }
return;
} }
} }
} }
@ -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$
}
} }
} }
} }

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

Loading…
Cancel
Save