diff --git a/astrid/common-src/com/localytics/android/Constants.java b/astrid/common-src/com/localytics/android/Constants.java index e21c1f90a..2befd3c6d 100644 --- a/astrid/common-src/com/localytics/android/Constants.java +++ b/astrid/common-src/com/localytics/android/Constants.java @@ -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() diff --git a/astrid/common-src/com/localytics/android/DatapointHelper.java b/astrid/common-src/com/localytics/android/DatapointHelper.java index 9debb28cd..5d42528d0 100755 --- a/astrid/common-src/com/localytics/android/DatapointHelper.java +++ b/astrid/common-src/com/localytics/android/DatapointHelper.java @@ -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. *
* 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; } diff --git a/astrid/common-src/com/localytics/android/JsonObjects.java b/astrid/common-src/com/localytics/android/JsonObjects.java index e7d7bf6ce..69d9bb15f 100644 --- a/astrid/common-src/com/localytics/android/JsonObjects.java +++ b/astrid/common-src/com/localytics/android/JsonObjects.java @@ -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} *
* 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} *
* 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} *
* 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} *
* 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} *
* 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} *
* 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} *
* 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} *
* 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() diff --git a/astrid/common-src/com/localytics/android/LocalyticsProvider.java b/astrid/common-src/com/localytics/android/LocalyticsProvider.java index 0d6f12a8d..d25e30a93 100644 --- a/astrid/common-src/com/localytics/android/LocalyticsProvider.java +++ b/astrid/common-src/com/localytics/android/LocalyticsProvider.java @@ -43,9 +43,10 @@ import java.util.Set; * Version history: *
* 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. *
* 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. *
* 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. *
* 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). *
* 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). *
* 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
* 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;
*
* 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;
*
* 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}
*
* 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;
*
* 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
*
* 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
*
* 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
*
* 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}.
*
* 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}.
*
* 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.
*
* 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()
diff --git a/astrid/common-src/com/localytics/android/LocalyticsSession.java b/astrid/common-src/com/localytics/android/LocalyticsSession.java
index 4c7c29498..248562947 100755
--- a/astrid/common-src/com/localytics/android/LocalyticsSession.java
+++ b/astrid/common-src/com/localytics/android/LocalyticsSession.java
@@ -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.
+ *
+ * This is a workaround for Android bug 3707
+ * This is a workaround for Android bug 3707
* This method must only be called after {@link #init()} is called.
*
- * 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
*
* This method must only be called after {@link #init()} is called.
*
- * 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
*
* This method must only be called after {@link #init()} is called.
*
- * 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
- * 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$
+ }
}
}
}
diff --git a/astrid/common-src/com/localytics/android/ReflectionUtils.java b/astrid/common-src/com/localytics/android/ReflectionUtils.java
index 476636823..a071b6b8a 100644
--- a/astrid/common-src/com/localytics/android/ReflectionUtils.java
+++ b/astrid/common-src/com/localytics/android/ReflectionUtils.java
@@ -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