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: *

    *
  1. 1: Initial version
  2. + *
  3. 2: No format changes--just deleting bad data stranded in the database
  4. *
*/ - private static final int DATABASE_VERSION = 1; + private static final int DATABASE_VERSION = 2; /** * Singleton instance of the {@link LocalyticsProvider}. Lazily initialized via {@link #getInstance(Context, String)}. @@ -76,13 +77,13 @@ import java.util.Set; *

* 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 getValidTables() @@ -380,7 +381,7 @@ import java.util.Set; * Private helper that deletes files from older versions of the Localytics library. *

* 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 . + */ + 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. + *

+ * This is a workaround for Android bug 3707 . + */ + private static final String EVENTS_SORT_ORDER = String.format("CAST(%s as TEXT)", EventsDbColumns._ID); //$NON-NLS-1$ + /** * Application context */ @@ -952,29 +966,30 @@ public final class LocalyticsSession return; } - /* - * Update the database. Note that there is a possible data loss condition: if the OPT_OUT flag is written in the API - * keys table but the process terminates before the opt-out event is written, then the client will stop collecting new - * data but the server won't ever receive the opt-out message which will cause data to be deleted. This is not - * expected to be likely, and generally still meets user expectations because no new data will be uploaded. - */ - final ContentValues values = new ContentValues(); - values.put(ApiKeysDbColumns.OPT_OUT, Boolean.valueOf(isOptingOut)); - mProvider.update(ApiKeysDbColumns.TABLE_NAME, values, String.format("%s = ?", ApiKeysDbColumns._ID), new String[] { Long.toString(mApiKeyId) }); //$NON-NLS-1$ - - if (!mIsSessionOpen) - { - /* - * Force a session to contain the opt event - */ - open(true); - tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null); - close(); - } - else + mProvider.runBatchTransaction(new Runnable() { - tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null); - } + @Override + public void run() + { + final ContentValues values = new ContentValues(); + values.put(ApiKeysDbColumns.OPT_OUT, Boolean.valueOf(isOptingOut)); + mProvider.update(ApiKeysDbColumns.TABLE_NAME, values, String.format("%s = ?", ApiKeysDbColumns._ID), new String[] { Long.toString(mApiKeyId) }); //$NON-NLS-1$ + + if (!mIsSessionOpen) + { + /* + * Force a session to contain the opt event + */ + open(true); + tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null); + close(); + } + else + { + tagEvent(isOptingOut ? OPT_OUT_EVENT : OPT_IN_EVENT, null); + } + } + }); /* * Update the in-memory representation. It is important for the in-memory representation to be updated after the @@ -989,13 +1004,13 @@ public final class LocalyticsSession *

* 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 attributes) + /* package */void tagEvent(final String event, final Map attributes) { if (!mIsSessionOpen) { @@ -1444,16 +1520,13 @@ public final class LocalyticsSession if (cursor.moveToFirst()) { - if (cursor.moveToFirst()) + if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME)))) { - if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME)))) + if (Constants.IS_LOGGABLE) { - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("Suppressed duplicate screen %s", screen)); //$NON-NLS-1$ - } - return; + Log.v(Constants.LOG_TAG, String.format("Suppressed duplicate screen %s", screen)); //$NON-NLS-1$ } + return; } } } @@ -1482,7 +1555,7 @@ public final class LocalyticsSession /** * Conditionally adds a flow event if no flow event exists in the current upload blob. */ - /* package */void conditionallyAddFlowEvent() + private void conditionallyAddFlowEvent() { /* * Creating a flow "event" is required to act as a placeholder so that the uploader will know that an upload needs to @@ -1496,10 +1569,11 @@ public final class LocalyticsSession try { eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[] - { EventsDbColumns._ID }, String.format("%s = ?", EventsDbColumns.EVENT_NAME), new String[] { FLOW_EVENT }, EventsDbColumns._ID); //$NON-NLS-1$ + { EventsDbColumns._ID }, String.format("%s = ?", EventsDbColumns.EVENT_NAME), new String[] //$NON-NLS-1$ + { FLOW_EVENT }, EVENTS_SORT_ORDER); blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[] - { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UploadBlobEventsDbColumns.EVENTS_KEY_REF); + { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UPLOAD_BLOBS_EVENTS_SORT_ORDER); final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[] { EventsDbColumns._ID }, blob_eventsCursor, new String[] @@ -1547,7 +1621,7 @@ public final class LocalyticsSession * @effects Mutates the database by creating a new upload blob for all events that are unassociated at the time this * method is called. */ - public void preUploadBuildBlobs() + /* package */void preUploadBuildBlobs() { /* * Group all events that aren't part of an upload blob into a new blob. While this process is a linear algorithm that @@ -1568,11 +1642,10 @@ public final class LocalyticsSession { EventsDbColumns._ID, EventsDbColumns.EVENT_NAME, - EventsDbColumns.WALL_TIME }, null, null, EventsDbColumns._ID); + EventsDbColumns.WALL_TIME }, null, null, EVENTS_SORT_ORDER); - // eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[] {EventsDbColumns._ID}, String.format("%s != ? AND %s < ?", EventsDbColumns.EVENT_NAME, EventsDbColumns.WALL_TIME), new String[] {CLOSE_EVENT, Long.toString(System.currentTimeMillis() - Constants.SESSION_EXPIRATION)}, EventsDbColumns._ID); //$NON-NLS-1$ blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[] - { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UploadBlobEventsDbColumns.EVENTS_KEY_REF); + { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, null, null, UPLOAD_BLOBS_EVENTS_SORT_ORDER); final int idColumn = eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID); final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[] @@ -1648,13 +1721,13 @@ public final class LocalyticsSession * This method must only be called after {@link #init()} is called. The session does not need to be open for an upload to * occur. *

- * 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 Type that the method should return * @param classObject Class on which to invoke {@code methodName}. Cannot be null. * @param methodName Name of the method to invoke. Cannot be null. @@ -31,15 +31,14 @@ public final class ReflectionUtils * @return The result of invoking the named method on the given class for the args * @throws RuntimeException if the class or method doesn't exist */ - @SuppressWarnings("unchecked") public static T tryInvokeStatic(final Class classObject, final String methodName, final Class[] types, final Object[] args) { - return (T) helper(null, classObject, null, methodName, types, args); + return helper(null, classObject, null, methodName, types, args); } /** * Use reflection to invoke a static method for a class object and method name - * + * * @param Type that the method should return * @param className Name of the class on which to invoke {@code methodName}. Cannot be null. * @param methodName Name of the method to invoke. Cannot be null. @@ -48,15 +47,14 @@ public final class ReflectionUtils * @return The result of invoking the named method on the given class for the args * @throws RuntimeException if the class or method doesn't exist */ - @SuppressWarnings("unchecked") public static T tryInvokeStatic(final String className, final String methodName, final Class[] types, final Object[] args) { - return (T) helper(className, null, null, methodName, types, args); + return helper(className, null, null, methodName, types, args); } /** * Use reflection to invoke a static method for a class object and method name - * + * * @param Type that the method should return * @param target Object instance on which to invoke {@code methodName}. Cannot be null. * @param methodName Name of the method to invoke. Cannot be null. @@ -65,10 +63,9 @@ public final class ReflectionUtils * @return The result of invoking the named method on the given class for the args * @throws RuntimeException if the class or method doesn't exist */ - @SuppressWarnings("unchecked") public static T 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") diff --git a/astrid/src/com/todoroo/astrid/service/StatisticsService.java b/astrid/src/com/todoroo/astrid/service/StatisticsService.java index 7b944af5f..3d6d0cfb6 100644 --- a/astrid/src/com/todoroo/astrid/service/StatisticsService.java +++ b/astrid/src/com/todoroo/astrid/service/StatisticsService.java @@ -26,8 +26,10 @@ public class StatisticsService { if(dontCollectStatistics()) return; - if(localyticsSession != null) + if(localyticsSession != null) { + localyticsSession.open(); // Multiple calls to open are ok, we just need to make sure it gets reopened after pause return; + } localyticsSession = new LocalyticsSession(context.getApplicationContext(), Constants.LOCALYTICS_KEY); @@ -55,8 +57,9 @@ public class StatisticsService { if(dontCollectStatistics()) return; - if(localyticsSession != null) + if(localyticsSession != null) { localyticsSession.close(); + } } /**