- * This is not a public API. - */ -/* package */final class Constants -{ - - /** - * Version number of this library. This number is primarily important in terms of changes to the upload format. - */ - //@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.2"; //$NON-NLS-1$ - - /** - * The package name of the Localytics library. - */ - /* - * Note: This value cannot be changed without significant consequences to the data in the database. - */ - public static final String LOCALYTICS_PACKAGE_NAME = "com.localytics.android"; //$NON-NLS-1$ - - /** - * Maximum number of sessions to store on disk. - */ - public static final int MAX_NUM_SESSIONS = 10; - - /** - * Maximum number of attributes per event session. - */ - public static final int MAX_NUM_ATTRIBUTES = 10; - - /** - * Maximum characters in an event name or attribute key/value. - */ - public static final int MAX_NAME_LENGTH = 128; - - /** - * Milliseconds after which a session is considered closed and cannot be reattached to. - *
- * For example, if the user opens an app, presses home, and opens the app again in less than {@link #SESSION_EXPIRATION} - * milliseconds, that will count as one session rather than two sessions. - */ - public static long SESSION_EXPIRATION = 15 * DateUtils.SECOND_IN_MILLIS; - - /** - * logcat log tag - */ - public static final String LOG_TAG = "Localytics"; //$NON-NLS-1$ - - /** - * Boolean indicating whether logcat messages are enabled. - *
- * Before releasing a production version of an app, this should be set to false for privacy and performance reasons. When - * logging is enabled, sensitive information such as the device ID may be printed to the log. - */ - public static boolean IS_LOGGABLE = false; - - /** - * Flag indicating whether runtime method parameter checking is performed. - */ - 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() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } -} diff --git a/astrid/common-src/com/localytics/android/DatapointHelper.java b/astrid/common-src/com/localytics/android/DatapointHelper.java deleted file mode 100755 index 95a7d143e..000000000 --- a/astrid/common-src/com/localytics/android/DatapointHelper.java +++ /dev/null @@ -1,392 +0,0 @@ -//@formatter:off -/** - * DatapointHelper.java Copyright (C) 2011 Char Software Inc., DBA Localytics This code is provided under the Localytics Modified - * BSD License. A copy of this license has been distributed in a file called LICENSE with this source code. Please visit - * www.localytics.com for more information. - */ -//@formatter:on - -package com.localytics.android; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -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. - */ -/* package */final class DatapointHelper -{ - /** - * AndroidID known to be duplicated across many devices due to manufacturer bugs. - */ - private static final String INVALID_ANDROID_ID = "9774d56d682e549c"; //$NON-NLS-1$ - - /** - * The path to the device_id file in previous versions of the Localytics library - */ - private static final String LEGACY_DEVICE_ID_FILE = "/localytics/device_id"; //$NON-NLS-1$ - - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private DatapointHelper() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * @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. - 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; - } - - /** - * Gets a 1-way hashed value of the device's Android ID. This value is encoded using a SHA-256 one way hash and therefore - * cannot be used to determine what device this data came from. - * - * @param context The context used to access the settings resolver - * @return An 1-way hashed version of the {@link android.provider.Settings.Secure#ANDROID_ID}. May return null if an Android - * ID or the hashing algorithm is not available. - */ - public static String getAndroidIdHashOrNull(final Context context) - { - // Make sure a legacy version of the SDK didn't leave behind a device ID. - // If it did, this ID must be used to keep user counts accurate - final File fp = new File(context.getFilesDir() + LEGACY_DEVICE_ID_FILE); - if (fp.exists() && fp.length() > 0) - { - try - { - BufferedReader reader = null; - try - { - final char[] buf = new char[100]; - int numRead; - reader = new BufferedReader(new FileReader(fp), 128); - numRead = reader.read(buf); - final String deviceId = String.copyValueOf(buf, 0, numRead); - reader.close(); - return deviceId; - } - catch (final FileNotFoundException e) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$ - } - } - finally - { - if (null != reader) - { - reader.close(); - } - } - } - catch (final IOException e) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$ - } - } - } - - final String androidId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID); - if (androidId == null || androidId.toLowerCase().equals(INVALID_ANDROID_ID)) - { - return null; - } - - return getSha256(androidId); - } - - /** - * Gets a 1-way hashed value of the device's unique serial number. This value is encoded using a SHA-256 one way hash and - * therefore cannot be used to determine what device this data came from. - *
- * Note: {@link android.os.Build#SERIAL} was introduced in SDK 9. For older SDKs, this method will return null. - * - * @return An 1-way hashed version of the {@link android.os.Build#SERIAL}. May return null if a serial or the hashing - * algorithm is not available. - */ - /* - * Suppress JavaDoc warnings because the {@link android.os.Build#SERIAL} fails when built with SDK 4. - */ - public static String getSerialNumberHashOrNull() - { - /* - * Obtain the device serial number using reflection, since serial number was added in SDK 9 - */ - String serialNumber = null; - if (Constants.CURRENT_API_LEVEL >= 9) - { - try - { - serialNumber = (String) Build.class.getField("SERIAL").get(null); //$NON-NLS-1$ - } - catch (final Exception e) - { - /* - * This should never happen, as SERIAL is a public field added in SDK 9. - */ - throw new RuntimeException(e); - } - } - - if (serialNumber == null) - { - return null; - } - - return getSha256(serialNumber); - } - - /** - * Gets the device's telephony ID (e.g. IMEI/MEID). - *
- * Note: this method will return null if {@link permission#READ_PHONE_STATE} is not available. This method will also return - * null on devices that do not have telephony. - * - * @param context The context used to access the phone state. - * @return An the {@link TelephonyManager#getDeviceId()}. Null if an ID is not available, or if - * {@link permission#READ_PHONE_STATE} is not available. - */ - public static String getTelephonyDeviceIdOrNull(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.booleanValue()) - { - 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 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$ - } - } - - return id; - } - - /** - * Gets a 1-way hashed value of the device's IMEI/MEID ID. This value is encoded using a SHA-256 one way hash and cannot be - * used to determine what device this data came from. - *
- * Note: this method will return null if this is a non-telephony device. - *
- * Note: this method will return null if {@link permission#READ_PHONE_STATE} is not available. - * - * @param context The context used to access the phone state. - * @return An 1-way hashed version of the {@link TelephonyManager#getDeviceId()}. Null if an ID or the hashing algorithm is - * not available, or if {@link permission#READ_PHONE_STATE} is not available. - */ - public static String getTelephonyDeviceIdHashOrNull(final Context context) - { - final String id = getTelephonyDeviceIdOrNull(context); - - if (null == id) - { - return null; - } - - return getSha256(id); - } - - /** - * Determines the type of network this device is connected to. - * - * @param context the context used to access the device's WIFI - * @param telephonyManager The manager used to access telephony info - * @return The type of network, or unknown if the information is unavailable - */ - public static String getNetworkType(final Context context, final TelephonyManager telephonyManager) - { - if (context.getPackageManager().checkPermission(permission.ACCESS_WIFI_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) - { - final NetworkInfo wifiInfo = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getNetworkInfo(ConnectivityManager.TYPE_WIFI); - if (wifiInfo != null && wifiInfo.isConnectedOrConnecting()) - { - return "wifi"; //$NON-NLS-1$ - } - } - else - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "Application does not have permission ACCESS_WIFI_STATE; determining Wi-Fi connectivity is unavailable"); //$NON-NLS-1$ - } - } - - return "android_network_type_" + telephonyManager.getNetworkType(); //$NON-NLS-1$ - } - - /** - * 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$ - if (Constants.CURRENT_API_LEVEL > 3) - { - try - { - 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$ - } - } - } - return mfg; - } - - /** - * Gets the versionName of the application. - * - * @param context {@link Context}. Cannot be null. - * @return The application's version - */ - public static String getAppVersion(final Context context) - { - final PackageManager pm = context.getPackageManager(); - - try - { - final String versionName = pm.getPackageInfo(context.getPackageName(), 0).versionName; - - /* - * If there is no versionName in the Android Manifest, the versionName will be null. - */ - if (versionName == null) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "versionName was null--is a versionName attribute set in the Android Manifest?"); //$NON-NLS-1$ - } - - return "unknown"; //$NON-NLS-1$ - } - - return versionName; - } - catch (final PackageManager.NameNotFoundException e) - { - /* - * This should never occur--our own package must exist for this code to be running - */ - throw new RuntimeException(e); - } - } - - /** - * Helper method to generate a SHA-256 hash of a given String - * - * @param string String to hash. Cannot be null. - * @return hashed version of the string using SHA-256. - */ - /* package */static String getSha256(final String string) - { - if (Constants.ENABLE_PARAMETER_CHECKING) - { - if (null == string) - { - throw new IllegalArgumentException("string cannot be null"); //$NON-NLS-1$ - } - } - - try - { - final MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$ - final byte[] digest = md.digest(string.getBytes("UTF-8")); //$NON-NLS-1$ - final BigInteger hashedNumber = new BigInteger(1, digest); - return hashedNumber.toString(16); - } - catch (final NoSuchAlgorithmException e) - { - throw new RuntimeException(e); - } - catch (final UnsupportedEncodingException e) - { - throw new RuntimeException(e); - } - } -} diff --git a/astrid/common-src/com/localytics/android/ExceptionHandler.java b/astrid/common-src/com/localytics/android/ExceptionHandler.java deleted file mode 100644 index 8f0f4c2e7..000000000 --- a/astrid/common-src/com/localytics/android/ExceptionHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2012 Todoroo Inc - * - * See the file "LICENSE" for the full license governing this code. - */ -package com.localytics.android; - -import android.util.Log; - -/** - * Exception handler for background threads used by the Localytics library. - *
- * Analytics are secondary to any other functions performed by an app, which means that analytics should never cause an app to - * crash. This handler therefore suppresses all uncaught exceptions from the Localytics library. - */ -/* package */final class ExceptionHandler implements Thread.UncaughtExceptionHandler -{ - @Override - public void uncaughtException(final Thread thread, final Throwable throwable) - { - /* - * Wrap all the work done by the exception handler in a try-catch. It would be ironic if this exception handler itself - * caused the parent process to crash. - */ - try - { - if (Constants.IS_LOGGABLE) - { - Log.e(Constants.LOG_TAG, "Localytics library threw an uncaught exception", throwable); //$NON-NLS-1$ - } - - // TODO: Upload uncaught exceptions so that we can fix them - } - catch (final Exception e) - { - if (Constants.IS_LOGGABLE) - { - Log.e(Constants.LOG_TAG, "Exception handler threw an exception", e); //$NON-NLS-1$ - } - } - } -} diff --git a/astrid/common-src/com/localytics/android/JsonObjects.java b/astrid/common-src/com/localytics/android/JsonObjects.java deleted file mode 100644 index f1f76e64b..000000000 --- a/astrid/common-src/com/localytics/android/JsonObjects.java +++ /dev/null @@ -1,574 +0,0 @@ -/** - * Copyright (c) 2012 Todoroo Inc - * - * See the file "LICENSE" for the full license governing this code. - */ -package com.localytics.android; - -import org.json.JSONArray; - -import android.Manifest.permission; - -/** - * Set of constants for building JSON objects that get sent to the Localytics web service. - */ -/* package */final class JsonObjects -{ - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private JsonObjects() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * Set of constants for the blob header JSON object. - */ - public static final class BlobHeader - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private BlobHeader() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * Data type for the JSON object. - * - * @see #VALUE_DATA_TYPE - */ - public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$ - - /** - * @see #KEY_DATA_TYPE - */ - public static final String VALUE_DATA_TYPE = "h"; //$NON-NLS-1$ - - /** - * Timestamp when the app was first launched and the persistent storage was created. Represented as seconds since the Unix - * Epoch. (Note: This is SECONDS and not milliseconds. This requires care, because Android represents time as - * milliseconds). - */ - public static final String KEY_PERSISTENT_STORAGE_CREATION_TIME_SECONDS = "pa"; //$NON-NLS-1$ - - /** - * Sequence number. A monotonically increasing count for each new blob. - */ - public static final String KEY_SEQUENCE_NUMBER = "seq"; //$NON-NLS-1$ - - /** - * A UUID for the blob. - */ - public static final String KEY_UNIQUE_ID = "u"; //$NON-NLS-1$ - - /** - * A JSON Object for attributes for the session. - */ - public static final String KEY_ATTRIBUTES = "attrs"; //$NON-NLS-1$ - - /** - * Attributes under {@link BlobHeader#KEY_ATTRIBUTES} - */ - public static final class Attributes - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private Attributes() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * Type: {@code String} - *
- * Data connection type. - */ - public static final String KEY_DATA_CONNECTION = "dac"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Version name of the application, taken from the Android Manifest. - */ - public static final String KEY_CLIENT_APP_VERSION = "av"; //$NON-NLS-1$ - - /** - * Key which maps to the SHA-256 of the device's {@link android.provider.Settings.Secure#ANDROID_ID}. - */ - public static final String KEY_DEVICE_ANDROID_ID_HASH = "du"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- */ - public static final String KEY_DEVICE_COUNTRY = "dc"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Manufacturer of the device (e.g. HTC, Samsung, Motorola, Kyocera, etc.) - */ - public static final String KEY_DEVICE_MANUFACTURER = "dma"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Model of the device (e.g. dream, - */ - public static final String KEY_DEVICE_MODEL = "dmo"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Android version (e.g. 1.6 or 2.3.4). - */ - public static final String KEY_DEVICE_OS_VERSION = "dov"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Telephony ID of the device, if the device has telephony and the app has {@link permission#READ_PHONE_STATE}. - * Otherwise null. - */ - public static final String KEY_DEVICE_TELEPHONY_ID = "tdid"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Platform of the device. For Android devices, this is always "android" - */ - public static final String KEY_DEVICE_PLATFORM = "dp"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * SHA-256 hash of the device's serial number. Only reported for Android 2.3 or later. Otherwise null. - */ - public static final String KEY_DEVICE_SERIAL_HASH = "dms"; //$NON-NLS-1$ - - /** - * Type: {@code int} - *
- * SDK compatibility level of the device. - * - * @see android.os.Build.VERSION#SDK - */ - public static final String KEY_DEVICE_SDK_LEVEL = "dsdk"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * SHA-256 hash of the device's Telephony ID, if the device has telephony and the app has - * {@link permission#READ_PHONE_STATE}. Otherwise null. - */ - public static final String KEY_DEVICE_TELEPHONY_ID_HASH = "dtidh"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Country for the device's current locale settings - */ - public static final String KEY_LOCALE_COUNTRY = "dlc"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Language for the device's current locale settings - */ - public static final String KEY_LOCALE_LANGUAGE = "dll"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Api key - */ - public static final String KEY_LOCALYTICS_API_KEY = "au"; //$NON-NLS-1$ - - /** - * 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$ - - /** - * 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$ - - /** - * Type: {@code String} - *
- * Network carrier of the device - */ - public static final String KEY_NETWORK_CARRIER = "nca"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- */ - public static final String KEY_NETWORK_COUNTRY = "nc"; //$NON-NLS-1$ - - /** - * @see #KEY_LOCALYTICS_DATA_TYPE - */ - @SuppressWarnings("hiding") - public static final String VALUE_DATA_TYPE = "a"; //$NON-NLS-1$ - - /** - * Value for the platform. - * - * @see #KEY_DEVICE_PLATFORM - */ - public static final String VALUE_PLATFORM = "Android"; //$NON-NLS-1$ - } - } - - /** - * Set of constants for the session open event. - */ - /* package */static final class SessionOpen - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private SessionOpen() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * Type: {@code String} - *
- * Data type for the JSON object. - * - * @see #VALUE_DATA_TYPE - */ - public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$ - - /** - * @see #KEY_DATA_TYPE - */ - public static final String VALUE_DATA_TYPE = "s"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * Epoch timestamp when the session was started in seconds. - */ - public static final String KEY_WALL_TIME_SECONDS = "ct"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * UUID of the event, which is the same thing as the session UUID - */ - public static final String KEY_EVENT_UUID = "u"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * Count for the number of sessions - */ - public static final String KEY_COUNT = "nth"; //$NON-NLS-1$ - } - - /** - * Set of constants for the session close event. - */ - /* package */static final class SessionClose - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private SessionClose() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * Type: {@code String} - *
- * Data type for the JSON object. - * - * @see #VALUE_DATA_TYPE - */ - public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * UUID of the event. - */ - public static final String KEY_EVENT_UUID = "u"; //$NON-NLS-1$ - - /** - * Type: {@code String[]} (technically, a JSON array of strings) - *
- * Ordered list of flow events that occurred - */ - public static final String KEY_FLOW_ARRAY = "fl"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * Epoch timestamp when the session was started - */ - public static final String KEY_SESSION_LENGTH_SECONDS = "ctl"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * Start time of the parent session - */ - public static final String KEY_SESSION_START_TIME = "ss"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * UUID of the session. - */ - public static final String KEY_SESSION_UUID = "su"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * Epoch timestamp when the session was started in seconds. - */ - public static final String KEY_WALL_TIME_SECONDS = "ct"; //$NON-NLS-1$ - - /** - * Data type for close events. - * - * @see #KEY_DATA_TYPE - */ - public static final String VALUE_DATA_TYPE = "c"; //$NON-NLS-1$ - } - - /** - * Set of constants for the session event event. - */ - /* package */static final class SessionEvent - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private SessionEvent() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * 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$ - - /** - * Type: {@code long} - *
- * Epoch timestamp when the session was started in seconds. - */ - public static final String KEY_WALL_TIME_SECONDS = "ct"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * UUID of the session. - */ - public static final String KEY_SESSION_UUID = "su"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * UUID of the event. - */ - public static final String KEY_EVENT_UUID = "u"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * Name of the event. - */ - public static final String KEY_NAME = "n"; //$NON-NLS-1$ - - /** - * Type: {@code JSONObject}. - *
- * Maps to the attributes of the event. - *
- * Note that this key is optional. If it is present, it will point to a non-null value representing the attributes of the - * event. Otherwise the key will not exist, indicating the event had no attributes. - */ - public static final String KEY_ATTRIBUTES = "attrs"; //$NON-NLS-1$ - } - - /** - * Set of constants for the session opt in/out event - */ - /* package */static final class OptEvent - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private OptEvent() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * 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$ - - /** - * Type: {@code long} - *
- * Epoch timestamp when the session was started in seconds. - */ - public static final String KEY_WALL_TIME_SECONDS = "ct"; //$NON-NLS-1$ - - /** - * Type: {@code String} - *
- * API key - */ - public static final String KEY_API_KEY = "u"; //$NON-NLS-1$ - - /** - * Type: {@code boolean} - *
- * True to opt-out. False to opt-in - */ - public static final String KEY_OPT = "out"; //$NON-NLS-1$ - } - - /** - * Set of constants for the session flow event. - */ - /* package */static final class EventFlow - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private EventFlow() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * Type: {@code String} - *
- * Data type for the JSON object. - * - * @see #VALUE_DATA_TYPE - */ - public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * UUID of the event, which is the same thing as the session UUID - */ - public static final String KEY_EVENT_UUID = "u"; //$NON-NLS-1$ - - /** - * Type: {@code long} - *
- * Start time of the parents session. - */ - public static final String KEY_SESSION_START_TIME = "ss"; //$NON-NLS-1$ - - /** - * Type: {@code Element[]} (technically a {@link JSONArray} of {@link Element} objects) - *
- * Ordered set of new flow elements that occurred since the last upload for this session. - */ - public static final String KEY_FLOW_NEW = "nw"; //$NON-NLS-1$ - - /** - * Type: {@code Element[]} (technically a {@link JSONArray} of {@link Element} objects) - *
- * Ordered set of old flow elements that occurred during all previous uploads for this session. - */ - public static final String KEY_FLOW_OLD = "od"; //$NON-NLS-1$ - - /** - * @see #KEY_DATA_TYPE - */ - public static final String VALUE_DATA_TYPE = "f"; //$NON-NLS-1$ - - /** - * Flow event element that indicates the type and name of the flow event. - */ - /* package */static final class Element - { - /** - * Private constructor prevents instantiation - * - * @throws UnsupportedOperationException because this class cannot be instantiated. - */ - private Element() - { - throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ - } - - /** - * A flow event that was due to an {@link SessionEvent}. - */ - public static final String TYPE_EVENT = "e"; //$NON-NLS-1$ - - /** - * A flow event that was due to a screen event. - */ - public static final String TYPE_SCREEN = "s"; //$NON-NLS-1$ - } - } -} diff --git a/astrid/common-src/com/localytics/android/LocalyticsProvider.java b/astrid/common-src/com/localytics/android/LocalyticsProvider.java deleted file mode 100644 index 7ed58bdd1..000000000 --- a/astrid/common-src/com/localytics/android/LocalyticsProvider.java +++ /dev/null @@ -1,1149 +0,0 @@ -/** - * Copyright (c) 2012 Todoroo Inc - * - * See the file "LICENSE" for the full license governing this code. - */ -package com.localytics.android; - -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteQueryBuilder; -import android.provider.BaseColumns; -import android.util.Log; - -/** - * Implements the storage mechanism for the Localytics library. The interface and implementation are similar to a ContentProvider - * but modified to be better suited to a library. The interface is table-oriented, rather than Uri-oriented. - *
- * This is not a public API. - */ -/* package */final class LocalyticsProvider -{ - /** - * Name of the Localytics database, stored in the host application's {@link Context#getDatabasePath(String)}. - *
- * This is not a public API. - */ - /* - * This field is made package-accessible for unit testing. While the exact file name is arbitrary, this name was chosen to - * avoid collisions with app developers because it is sufficiently long and uses the Localytics package namespace. - */ - /* package */static final String DATABASE_FILE = "com.localytics.android.%s.sqlite"; //$NON-NLS-1$ - - /** - * Version of the database. - *
- * 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, final String apiKey) - { - /* - * Note: Don't call getApplicationContext() on the context, as that would return a different context and defeat useful - * contexts such as RenamingDelegatingContext. - */ - - if (Constants.ENABLE_PARAMETER_CHECKING) - { - if (null == context) - { - throw new IllegalArgumentException("context cannot be null"); //$NON-NLS-1$ - } - } - - /* - * Although RenamingDelegatingContext is part of the Android SDK, the class isn't present in the ClassLoader unless the - * process is being run as a unit test. For that reason, comparing class names is necessary instead of doing instanceof. - */ - if (context.getClass().getName().equals("android.test.RenamingDelegatingContext")) //$NON-NLS-1$ - { - return new LocalyticsProvider(context, apiKey); - } - - synchronized (sLocalyticsProviderIntrinsicLock) - { - LocalyticsProvider provider = sLocalyticsProviderMap.get(apiKey); - - if (null == provider) - { - provider = new LocalyticsProvider(context, apiKey); - sLocalyticsProviderMap.put(apiKey, provider); - } - - return provider; - } - } - - /** - * 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) - { - /* - * Rather than use the API key directly in the file name, it is put through SHA-256. The main reason for doing that is to - * decouple the requirements of the Android file system from the possible values of the API key string. There is a very, - * very small risk of a collision with the SHA-256 algorithm, but most clients will only have a single API key. Those with - * multiple keys may have 2 or 3, so the risk of a collision there is also very low. - */ - - mDb = new DatabaseHelper(context, String.format(DATABASE_FILE, DatapointHelper.getSha256(apiKey)), DATABASE_VERSION).getWritableDatabase(); - } - - /** - * 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. - * @throws IllegalArgumentException if tableName is null or not a valid table name. - * @throws IllegalArgumentException if values are null. - */ - public long insert(final String tableName, final ContentValues values) - { - if (Constants.ENABLE_PARAMETER_CHECKING) - { - if (!isValidTable(tableName)) - { - throw new IllegalArgumentException(String.format("tableName %s is invalid", tableName)); //$NON-NLS-1$ - } - - if (null == values) - { - throw new IllegalArgumentException("values cannot be null"); //$NON-NLS-1$ - } - } - - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("Insert table: %s, values: %s", tableName, values.toString())); //$NON-NLS-1$ - } - - final long result = mDb.insertOrThrow(tableName, null, values); - - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("Inserted row with new id %d", Long.valueOf(result))); //$NON-NLS-1$ - } - - return result; - } - - /** - * 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 - * may contain ? symbols, which will be replaced by values from the {@code selectionArgs} param. - * @param selectionArgs An optional string array of replacements for ? symbols in {@code selection}. May be null. - * @param sortOrder How the rows in the cursor should be sorted. If null, then the sort order is undefined. - * @return Cursor for the query. To the receiver: Don't forget to call .close() on the cursor when finished with it. - * @throws IllegalArgumentException if tableName is null or not a valid table name. - */ - public Cursor query(final String tableName, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) - { - if (Constants.ENABLE_PARAMETER_CHECKING) - { - if (!isValidTable(tableName)) - { - throw new IllegalArgumentException(String.format("tableName %s is invalid", tableName)); //$NON-NLS-1$ - } - } - - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("Query table: %s, projection: %s, selection: %s, selectionArgs: %s", tableName, Arrays.toString(projection), selection, Arrays.toString(selectionArgs))); //$NON-NLS-1$ - } - - final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - qb.setTables(tableName); - - final Cursor result = qb.query(mDb, projection, selection, selectionArgs, null, null, sortOrder); - - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, "Query result is: " + DatabaseUtils.dumpCursorToString(result)); //$NON-NLS-1$ - } - - return result; - } - - /** - * 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. - * @param selection A filter to limit which rows are updated, 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. - * @param selectionArgs An optional string array of replacements for ? symbols in {@code selection}. May be null. - * @return int representing the number of rows modified, which is in the range from 0 to the number of items in the table. - * @throws IllegalArgumentException if tableName is null or not a valid table name. - */ - public int update(final String tableName, final ContentValues values, final String selection, final String[] selectionArgs) - { - if (Constants.ENABLE_PARAMETER_CHECKING) - { - if (!isValidTable(tableName)) - { - throw new IllegalArgumentException(String.format("tableName %s is invalid", tableName)); //$NON-NLS-1$ - } - } - - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("Update table: %s, values: %s, selection: %s, selectionArgs: %s", tableName, values.toString(), selection, Arrays.toString(selectionArgs))); //$NON-NLS-1$ - } - - return mDb.update(tableName, values, selection, selectionArgs); - } - - /** - * 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.
- * @param selectionArgs An optional string array of replacements for ? symbols in {@code selection}. May be null.
- * @return The number of rows affected, which is in the range from 0 to the number of items in the table.
- * @throws IllegalArgumentException if tableName is null or not a valid table name.
- */
- public int delete(final String tableName, final String selection, final String[] selectionArgs)
- {
- if (Constants.ENABLE_PARAMETER_CHECKING)
- {
- if (!isValidTable(tableName))
- {
- throw new IllegalArgumentException(String.format("tableName %s is invalid", tableName)); //$NON-NLS-1$
- }
- }
-
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, String.format("Delete table: %s, selection: %s, selectionArgs: %s", tableName, selection, Arrays.toString(selectionArgs))); //$NON-NLS-1$
- }
-
- final int count;
- if (null == selection)
- {
- count = mDb.delete(tableName, "1", null); //$NON-NLS-1$
- }
- else
- {
- count = mDb.delete(tableName, selection, selectionArgs);
- }
-
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, String.format("Deleted %d rows", Integer.valueOf(count))); //$NON-NLS-1$
- }
-
- return count;
- }
-
- /**
- * 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.
- */
- public void runBatchTransaction(final Runnable runnable)
- {
- if (Constants.ENABLE_PARAMETER_CHECKING)
- {
- if (null == runnable)
- {
- throw new IllegalArgumentException("runnable cannot be null"); //$NON-NLS-1$
- }
- }
-
- mDb.beginTransaction();
- try
- {
- runnable.run();
-
- mDb.setTransactionSuccessful();
- }
- finally
- {
- mDb.endTransaction();
- }
- }
-
- /**
- * 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.
- */
- private static boolean isValidTable(final String table)
- {
- if (null == table)
- {
- return false;
- }
-
- if (!sValidTables.contains(table))
- {
- return false;
- }
-
- return true;
- }
-
- /**
- * 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
- */
- /* package */static void deleteOldFiles(final Context context)
- {
- if (Constants.ENABLE_PARAMETER_CHECKING)
- {
- if (null == context)
- {
- throw new IllegalArgumentException("context cannot be null"); //$NON-NLS-1$
- }
- }
-
- deleteDirectory(new File(context.getFilesDir(), "localytics")); //$NON-NLS-1$
- }
-
- /**
- * 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.
- */
- private static boolean deleteDirectory(final File directory)
- {
- if (directory.exists() && directory.isDirectory())
- {
- for (final String child : directory.list())
- {
- final boolean success = deleteDirectory(new File(directory, child));
- if (!success)
- {
- return false;
- }
- }
- }
-
- // The directory is now empty so delete it
- return directory.delete();
- }
-
- /**
- * A private helper class to open and create the Localytics SQLite database.
- */
- private static final class DatabaseHelper extends SQLiteOpenHelper
- {
- /**
- * Constant representing the SQLite value for true
- */
- private static final String SQLITE_BOOLEAN_TRUE = "1"; //$NON-NLS-1$
-
- /**
- * Constant representing the SQLite value for false
- */
- private static final String SQLITE_BOOLEAN_FALSE = "0"; //$NON-NLS-1$
-
- /**
- * @param context Application context. Cannot be null.
- * @param name File name of the database. Cannot be null or empty. A database with this name will be opened in
- * {@link Context#getDatabasePath(String)}.
- * @param version version of the database.
- */
- public DatabaseHelper(final Context context, final String name, final int version)
- {
- super(context, name, null, version);
- }
-
- /**
- * Initializes the tables of the database.
- *
- * 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
- */
- @Override
- public void onCreate(final SQLiteDatabase db)
- {
- if (null == db)
- {
- throw new IllegalArgumentException("db cannot be null"); //$NON-NLS-1$
- }
-
- // api_keys table
- db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s TEXT UNIQUE NOT NULL, %s TEXT UNIQUE NOT NULL, %s INTEGER NOT NULL CHECK (%s >= 0), %s INTEGER NOT NULL CHECK(%s IN (%s, %s)));", ApiKeysDbColumns.TABLE_NAME, ApiKeysDbColumns._ID, ApiKeysDbColumns.API_KEY, ApiKeysDbColumns.UUID, ApiKeysDbColumns.CREATED_TIME, ApiKeysDbColumns.CREATED_TIME, ApiKeysDbColumns.OPT_OUT, ApiKeysDbColumns.OPT_OUT, SQLITE_BOOLEAN_FALSE, SQLITE_BOOLEAN_TRUE)); //$NON-NLS-1$
-
- // sessions table
- db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s INTEGER REFERENCES %s(%s) NOT NULL, %s TEXT UNIQUE NOT NULL, %s INTEGER NOT NULL CHECK (%s >= 0), %s TEXT NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT);", SessionsDbColumns.TABLE_NAME, SessionsDbColumns._ID, SessionsDbColumns.API_KEY_REF, ApiKeysDbColumns.TABLE_NAME, ApiKeysDbColumns._ID, SessionsDbColumns.UUID, SessionsDbColumns.SESSION_START_WALL_TIME, SessionsDbColumns.SESSION_START_WALL_TIME, SessionsDbColumns.LOCALYTICS_LIBRARY_VERSION, SessionsDbColumns.APP_VERSION, SessionsDbColumns.ANDROID_VERSION, SessionsDbColumns.ANDROID_SDK, SessionsDbColumns.DEVICE_MODEL, SessionsDbColumns.DEVICE_MANUFACTURER, SessionsDbColumns.DEVICE_ANDROID_ID_HASH, SessionsDbColumns.DEVICE_TELEPHONY_ID, SessionsDbColumns.DEVICE_TELEPHONY_ID_HASH, SessionsDbColumns.DEVICE_SERIAL_NUMBER_HASH, SessionsDbColumns.LOCALE_LANGUAGE, SessionsDbColumns.LOCALE_COUNTRY, SessionsDbColumns.NETWORK_CARRIER, SessionsDbColumns.NETWORK_COUNTRY, SessionsDbColumns.NETWORK_TYPE, SessionsDbColumns.DEVICE_COUNTRY, SessionsDbColumns.LATITUDE, SessionsDbColumns.LONGITUDE)); //$NON-NLS-1$
-
- // events table
- db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s INTEGER REFERENCES %s(%s) NOT NULL, %s TEXT UNIQUE NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL CHECK (%s >= 0), %s INTEGER NOT NULL CHECK (%s >= 0));", EventsDbColumns.TABLE_NAME, EventsDbColumns._ID, EventsDbColumns.SESSION_KEY_REF, SessionsDbColumns.TABLE_NAME, SessionsDbColumns._ID, EventsDbColumns.UUID, EventsDbColumns.EVENT_NAME, EventsDbColumns.REAL_TIME, EventsDbColumns.REAL_TIME, EventsDbColumns.WALL_TIME, EventsDbColumns.WALL_TIME)); //$NON-NLS-1$
-
- // event_history table
- /*
- * Note: the events history should be using foreign key constrains on the upload blobs table, but that is currently
- * disabled to simplify the implementation of the upload processing.
- */
- db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s INTEGER REFERENCES %s(%s) NOT NULL, %s TEXT NOT NULL CHECK(%s IN (%s, %s)), %s TEXT NOT NULL, %s INTEGER);", EventHistoryDbColumns.TABLE_NAME, EventHistoryDbColumns._ID, EventHistoryDbColumns.SESSION_KEY_REF, SessionsDbColumns.TABLE_NAME, SessionsDbColumns._ID, EventHistoryDbColumns.TYPE, EventHistoryDbColumns.TYPE, Integer.valueOf(EventHistoryDbColumns.TYPE_EVENT), Integer.valueOf(EventHistoryDbColumns.TYPE_SCREEN), EventHistoryDbColumns.NAME, EventHistoryDbColumns.PROCESSED_IN_BLOB)); //$NON-NLS-1$
- //db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s INTEGER REFERENCES %s(%s) NOT NULL, %s TEXT NOT NULL CHECK(%s IN (%s, %s)), %s TEXT NOT NULL, %s INTEGER REFERENCES %s(%s));", EventHistoryDbColumns.TABLE_NAME, EventHistoryDbColumns._ID, EventHistoryDbColumns.SESSION_KEY_REF, SessionsDbColumns.TABLE_NAME, SessionsDbColumns._ID, EventHistoryDbColumns.TYPE, EventHistoryDbColumns.TYPE, Integer.valueOf(EventHistoryDbColumns.TYPE_EVENT), Integer.valueOf(EventHistoryDbColumns.TYPE_SCREEN), EventHistoryDbColumns.NAME, EventHistoryDbColumns.PROCESSED_IN_BLOB, UploadBlobsDbColumns.TABLE_NAME, UploadBlobsDbColumns._ID)); //$NON-NLS-1$
-
- // attributes table
- db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s INTEGER REFERENCES %s(%s) NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL);", AttributesDbColumns.TABLE_NAME, AttributesDbColumns._ID, AttributesDbColumns.EVENTS_KEY_REF, EventsDbColumns.TABLE_NAME, EventsDbColumns._ID, AttributesDbColumns.ATTRIBUTE_KEY, AttributesDbColumns.ATTRIBUTE_VALUE)); //$NON-NLS-1$
-
- // upload blobs
- db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s TEXT UNIQUE NOT NULL);", UploadBlobsDbColumns.TABLE_NAME, UploadBlobsDbColumns._ID, UploadBlobsDbColumns.UUID)); //$NON-NLS-1$
-
- // upload events
- db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s INTEGER REFERENCES %s(%s) NOT NULL, %s INTEGER REFERENCES %s(%s) NOT NULL);", UploadBlobEventsDbColumns.TABLE_NAME, UploadBlobEventsDbColumns._ID, UploadBlobEventsDbColumns.UPLOAD_BLOBS_KEY_REF, UploadBlobsDbColumns.TABLE_NAME, UploadBlobsDbColumns._ID, UploadBlobEventsDbColumns.EVENTS_KEY_REF, EventsDbColumns.TABLE_NAME, EventsDbColumns._ID)); //$NON-NLS-1$
- }
-
- @Override
- public void onOpen(final SQLiteDatabase db)
- {
- super.onOpen(db);
-
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, String.format("SQLite library version is: %s", DatabaseUtils.stringForQuery(db, "select sqlite_version()", null))); //$NON-NLS-1$//$NON-NLS-2$
- }
-
- if (!db.isReadOnly())
- {
- /*
- * Enable foreign key support
- */
- db.execSQL("PRAGMA foreign_keys = ON;"); //$NON-NLS-1$
-
- // if (Constants.IS_LOGGABLE)
- // {
- // try
- // {
- // final String result1 = DatabaseUtils.stringForQuery(db, "PRAGMA foreign_keys;", null); //$NON-NLS-1$
- // Log.v(Constants.LOG_TAG, String.format("Foreign keys support result was: %s", result1)); //$NON-NLS-1$
- // }
- // catch (final SQLiteDoneException e)
- // {
- // Log.w(Constants.LOG_TAG, e);
- // }
- // }
- }
- }
-
- @Override
- public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion)
- {
- 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)
- // {
- // }
- }
-
- /**
- * Table for the API keys used and the opt-out preferences for each API key.
- *
- * This is not a public API.
- */
- public static final class ApiKeysDbColumns implements BaseColumns
- {
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private ApiKeysDbColumns()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * SQLite table name
- */
- public static final String TABLE_NAME = "api_keys"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * The Localytics API key.
- *
- * Constraints: This column is unique and cannot be null.
- */
- public static final String API_KEY = "api_key"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * A UUID for the installation.
- *
- * Constraints: This column is unique and cannot be null.
- */
- public static final String UUID = "uuid"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code boolean}
- *
- * A flag indicating whether the user has opted out of data collection.
- *
- * Constraints: This column must be in the set {0, 1} and cannot be null.
- */
- public static final String OPT_OUT = "opt_out"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A long representing the {@link System#currentTimeMillis()} when the row was created. Once created, this row will not be
- * modified.
- *
- * Constraints: This column must be >=0. This column cannot be null.
- */
- public static final String CREATED_TIME = "created_time"; //$NON-NLS-1$
- }
-
- /**
- * Database table for the session attributes. There is a one-to-many relationship between one event in the
- * {@link EventsDbColumns} table and the many attributes associated with that event.
- *
- * This is not a public API.
- */
- public static final class AttributesDbColumns implements BaseColumns
- {
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private AttributesDbColumns()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * SQLite table name
- */
- public static final String TABLE_NAME = "attributes"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A one-to-many relationship with {@link EventsDbColumns#_ID}.
- *
- * Constraints: This is a foreign key with the {@link EventsDbColumns#_ID} column. This cannot be null.
- */
- public static final String EVENTS_KEY_REF = "events_key_ref"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the key name of the attribute.
- *
- * Constraints: This cannot be null.
- */
- public static final String ATTRIBUTE_KEY = "attribute_key"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the value of the attribute.
- *
- * Constraints: This cannot be null.
- */
- public static final String ATTRIBUTE_VALUE = "attribute_value"; //$NON-NLS-1$
-
- }
-
- /**
- * Database table for the session events. There is a one-to-many relationship between one session data entry in the
- * {@link SessionsDbColumns} table and the many events associated with that session.
- *
- * This is not a public API.
- */
- public static final class EventsDbColumns implements BaseColumns
- {
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private EventsDbColumns()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * SQLite table name
- */
- public static final String TABLE_NAME = "events"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A one-to-many relationship with {@link SessionsDbColumns#_ID}.
- *
- * Constraints: This is a foreign key with the {@link SessionsDbColumns#_ID} column. This cannot be null.
- */
- public static final String SESSION_KEY_REF = "session_key_ref"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Unique ID of the event, as generated from {@link java.util.UUID}.
- *
- * Constraints: This is unique and cannot be null.
- */
- public static final String UUID = "uuid"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the name of the event.
- *
- * Constraints: This cannot be null.
- */
- public static final String EVENT_NAME = "event_name"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A long representing the {@link android.os.SystemClock#elapsedRealtime()} when the event occurred.
- *
- * Constraints: This column must be >=0. This column cannot be null.
- */
- public static final String REAL_TIME = "real_time"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A long representing the {@link System#currentTimeMillis()} when the event occurred.
- *
- * Constraints: This column must be >=0. This column cannot be null.
- */
- public static final String WALL_TIME = "wall_time"; //$NON-NLS-1$
-
- }
-
- /**
- * Database table for tracking the history of events and screens. There is a one-to-many relationship between one session data
- * entry in the {@link SessionsDbColumns} table and the many historical events associated with that session.
- *
- * This is not a public API.
- */
- public static final class EventHistoryDbColumns implements BaseColumns
- {
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private EventHistoryDbColumns()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * SQLite table name
- */
- public static final String TABLE_NAME = "event_history"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A one-to-many relationship with {@link SessionsDbColumns#_ID}.
- *
- * Constraints: This is a foreign key with the {@link SessionsDbColumns#_ID} column. This cannot be null.
- */
- public static final String SESSION_KEY_REF = "session_key_ref"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Unique ID of the event, as generated from {@link java.util.UUID}.
- *
- * Constraints: This is unique and cannot be null.
- */
- public static final String TYPE = "type"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the name of the screen or event.
- *
- * Constraints: This cannot be null.
- */
- public static final String NAME = "name"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code boolean}
- *
- * Foreign key to the upload blob that this event was processed in. May be null indicating that this event wasn't
- * processed yet.
- */
- public static final String PROCESSED_IN_BLOB = "processed_in_blob"; //$NON-NLS-1$
-
- /**
- * Type value for {@link #TYPE} indicates an event event.
- */
- public static final int TYPE_EVENT = 0;
-
- /**
- * Type value for {@link #TYPE} that indicates a screen event.
- */
- public static final int TYPE_SCREEN = 1;
- }
-
- /**
- * Database table for the session data. There is a one-to-many relationship between one API key entry in the
- * {@link ApiKeysDbColumns} table and many sessions for that API key.
- *
- * This is not a public API.
- */
- public static final class SessionsDbColumns implements BaseColumns
- {
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private SessionsDbColumns()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * SQLite table name
- */
- public static final String TABLE_NAME = "sessions"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A one-to-one relationship with {@link ApiKeysDbColumns#_ID}.
- *
- * Constraints: This is a foreign key with the {@link ApiKeysDbColumns#_ID} column. This cannot be null.
- */
- public static final String API_KEY_REF = "api_key_ref"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Unique ID of the event, as generated from {@link java.util.UUID}.
- *
- * Constraints: This is unique and cannot be null.
- */
- public static final String UUID = "uuid"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * The wall time when the session started.
- *
- * Constraints: This column must be >=0. This column cannot be null.
- */
- /*
- * Note: While this same information is encoded in {@link EventsDbColumns#WALL_TIME} for the session open event, that row
- * may not be available when an upload occurs and the upload needs to compute the duration of the session.
- */
- public static final String SESSION_START_WALL_TIME = "session_start_wall_time"; //$NON-NLS-1$
-
- /**
- * 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$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the app's versionName
- *
- * Constraints: This cannot be null.
- */
- public static final String APP_VERSION = "app_version"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the version of Android
- *
- * Constraints: This cannot be null.
- */
- public static final String ANDROID_VERSION = "android_version"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code int}
- *
- * Integer the Android SDK
- *
- * Constraints: Must be an integer and cannot be null.
- *
- * @see android.os.Build.VERSION#SDK
- */
- public static final String ANDROID_SDK = "android_sdk"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the device model
- *
- * Constraints: None
- *
- * @see android.os.Build#MODEL
- */
- public static final String DEVICE_MODEL = "device_model"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the device manufacturer
- *
- * Constraints: None
- *
- * @see android.os.Build#MANUFACTURER
- */
- public static final String DEVICE_MANUFACTURER = "device_manufacturer"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * 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$
-
- /**
- * TYPE: {@code String}
- *
- * String representing the telephony ID of the device. May be null for non-telephony devices. May also be null 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 = "device_telephony_id"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * String representing a hash of the telephony ID of the device. May be null for non-telephony devices. May also be null
- * 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$
-
- /**
- * TYPE: {@code String}
- *
- * String representing a hash of the the serial number of the device. May be null for some telephony devices.
- *
- * Constraints: None
- */
- public static final String DEVICE_SERIAL_NUMBER_HASH = "device_serial_number_hash"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Represents the locale language of the device.
- *
- * Constraints: Cannot be null.
- */
- public static final String LOCALE_LANGUAGE = "locale_language"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Represents the locale country of the device.
- *
- * Constraints: Cannot be null.
- */
- public static final String LOCALE_COUNTRY = "locale_country"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Represents the locale country of the device, according to the SIM card.
- *
- * Constraints: Cannot be null.
- */
- public static final String DEVICE_COUNTRY = "device_country"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Represents the network carrier of the device. May be null for non-telephony devices.
- *
- * Constraints: None
- */
- public static final String NETWORK_CARRIER = "network_carrier"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Represents the network country of the device. May be null for non-telephony devices.
- *
- * Constraints: None
- */
- public static final String NETWORK_COUNTRY = "network_country"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Represents the primary network connection type for the device. This could be any type, including Wi-Fi, various cell
- * networks, Ethernet, etc.
- *
- * Constraints: None
- *
- * @see android.telephony.TelephonyManager
- */
- public static final String NETWORK_TYPE = "network_type"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code double}
- *
- * Represents the latitude of the device. May be null if no longitude is known.
- *
- * Constraints: None
- */
- public static final String LATITUDE = "latitude"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code double}
- *
- * Represents the longitude of the device. May be null if no longitude is known.
- *
- * Constraints: None
- */
- public static final String LONGITUDE = "longitude"; //$NON-NLS-1$
-
- }
-
- /**
- * Database table for the events associated with a given upload blob. There is a one-to-many relationship between one upload
- * blob in the {@link UploadBlobsDbColumns} table and the blob events. There is a one-to-one relationship between each blob
- * event entry and the actual events in the {@link EventsDbColumns} table. *
- *
- * This is not a public API.
- */
- public static final class UploadBlobEventsDbColumns implements BaseColumns
- {
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private UploadBlobEventsDbColumns()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * SQLite table name
- */
- public static final String TABLE_NAME = "upload_blob_events"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * A one-to-many relationship with {@link UploadBlobsDbColumns#_ID}.
- *
- * Constraints: This is a foreign key with the {@link UploadBlobsDbColumns#_ID} column. This cannot be null.
- */
- public static final String UPLOAD_BLOBS_KEY_REF = "upload_blobs_key_ref"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code long}
- *
- * A one-to-one relationship with {@link EventsDbColumns#_ID}.
- *
- * Constraints: This is a foreign key with the {@link EventsDbColumns#_ID} column. This cannot be null.
- */
- public static final String EVENTS_KEY_REF = "events_key_ref"; //$NON-NLS-1$
- }
-
- /**
- * Database table for the upload blobs. Logically, a blob owns many events. In terms of the implementation, some indirection
- * is introduced by a blob having a one-to-many relationship with {@link UploadBlobsDbColumns} and
- * {@link UploadBlobsDbColumns} having a one-to-one relationship with {@link EventsDbColumns}
- *
- * This is not a public API.
- */
- public static final class UploadBlobsDbColumns implements BaseColumns
- {
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private UploadBlobsDbColumns()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * SQLite table name
- */
- public static final String TABLE_NAME = "upload_blobs"; //$NON-NLS-1$
-
- /**
- * TYPE: {@code String}
- *
- * Unique ID of the upload blob, as generated from {@link java.util.UUID}.
- *
- * Constraints: This is unique and cannot be null.
- */
- public static final String UUID = "uuid"; //$NON-NLS-1$
-
- }
-}
diff --git a/astrid/common-src/com/localytics/android/LocalyticsSession.java b/astrid/common-src/com/localytics/android/LocalyticsSession.java
deleted file mode 100755
index 3ef2f0881..000000000
--- a/astrid/common-src/com/localytics/android/LocalyticsSession.java
+++ /dev/null
@@ -1,2765 +0,0 @@
-// @formatter:off
-/*
- * LocalyticsSession.java Copyright (C) 2011 Char Software Inc., DBA Localytics This code is provided under the Localytics
- * Modified BSD License. A copy of this license has been distributed in a file called LICENSE with this source code. Please visit
- * www.localytics.com for more information.
- */
-// @formatter:on
-
-package com.localytics.android;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.UUID;
-import java.util.zip.GZIPOutputStream;
-
-import org.apache.http.HttpResponse;
-import org.apache.http.StatusLine;
-import org.apache.http.client.ClientProtocolException;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.ByteArrayEntity;
-import org.apache.http.impl.client.DefaultHttpClient;
-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;
-import com.localytics.android.LocalyticsProvider.EventHistoryDbColumns;
-import com.localytics.android.LocalyticsProvider.EventsDbColumns;
-import com.localytics.android.LocalyticsProvider.SessionsDbColumns;
-import com.localytics.android.LocalyticsProvider.UploadBlobEventsDbColumns;
-import com.localytics.android.LocalyticsProvider.UploadBlobsDbColumns;
-
-/**
- * This class manages creating, collecting, and uploading a Localytics session. Please see the following guides for information on
- * how to best use this library, sample code, and other useful information:
- *
- * Permissions required:
- *
- * This library will create a database called "com.android.localytics.sqlite" within the host application's
- * {@link Context#getDatabasePath(String)} directory. For security, this file directory will be created
- * {@link Context#MODE_PRIVATE}. The host application must not modify this database file. If the host application implements a
- * backup/restore mechanism, such as {@code android.app.backup.BackupManager}, the host application should not worry about backing
- * up the data in the Localytics database.
- *
- * This library is thread-safe but is not multi-process safe. Unless the application explicitly uses different process attributes
- * in the Android Manifest, this is not an issue.
- *
- * This class is thread-safe.
- *
- * @version 2.0
- */
-public final class LocalyticsSession
-{
- /*
- * DESIGN NOTES
- *
- * The LocalyticsSession stores all of its state as a SQLite database in the parent application's private database storage
- * directory.
- *
- * Every action performed within (open, close, opt-in, opt-out, customer events) are all treated as events by the library.
- * Events are given a package prefix to ensure a namespace without collisions. Events internal to the library are flagged with
- * the Localytics package name, while events from the customer's code are flagged with the customer's package name. There's no
- * need to worry about the customer changing the package name and disrupting the naming convention, as changing the package
- * name means that a new user is created in Android and the app with a new package name gets its own storage directory.
- *
- *
- * MULTI-THREADING
- *
- * The LocalyticsSession stores all of its state as a SQLite database in the parent application's private database storage
- * directory. Disk access is slow and can block the UI in Android, so the LocalyticsSession object is a wrapper around a pair
- * of Handler objects, with each Handler object running on its own separate thread.
- *
- * All requests made of the LocalyticsSession are passed along to the mSessionHandler object, which does most of the work. The
- * mSessionHandler will pass off upload requests to the mUploadHandler, to prevent the mSessionHandler from being blocked by
- * network traffic.
- *
- * If an upload request is made, the mSessionHandler will set a flag that an upload is in progress (this flag is important for
- * thread-safety of the session data stored on disk). Then the upload request is passed to the mUploadHandler's queue. If a
- * second upload request is made while the first one is underway, the mSessionHandler notifies the mUploadHandler, which will
- * notify the mSessionHandler to retry that upload request when the first upload is completed.
- *
- * Although each LocalyticsSession object will have its own unique instance of mSessionHandler, thread-safety is handled by
- * using a single sSessionHandlerThread.
- */
-
- /**
- * Format string for events
- */
- /* package */static final String EVENT_FORMAT = "%s:%s"; //$NON-NLS-1$
-
- /**
- * Open event
- */
- /* package */static final String OPEN_EVENT = String.format(EVENT_FORMAT, Constants.LOCALYTICS_PACKAGE_NAME, "open"); //$NON-NLS-1$
-
- /**
- * Close event
- */
- /* package */static final String CLOSE_EVENT = String.format(EVENT_FORMAT, Constants.LOCALYTICS_PACKAGE_NAME, "close"); //$NON-NLS-1$
-
- /**
- * Opt-in event
- */
- /* package */static final String OPT_IN_EVENT = String.format(EVENT_FORMAT, Constants.LOCALYTICS_PACKAGE_NAME, "opt_in"); //$NON-NLS-1$
-
- /**
- * Opt-out event
- */
- /* package */static final String OPT_OUT_EVENT = String.format(EVENT_FORMAT, Constants.LOCALYTICS_PACKAGE_NAME, "opt_out"); //$NON-NLS-1$
-
- /**
- * Flow event
- */
- /* package */static final String FLOW_EVENT = String.format(EVENT_FORMAT, Constants.LOCALYTICS_PACKAGE_NAME, "flow"); //$NON-NLS-1$
-
- /**
- * Background thread used for all Localytics session processing. This thread is shared across all instances of
- * LocalyticsSession within a process.
- */
- /*
- * By using the class name for the HandlerThread, obfuscation through Proguard is more effective: if Proguard changes the
- * class name, the thread name also changes.
- */
- private static final HandlerThread sSessionHandlerThread = getHandlerThread(SessionHandler.class.getSimpleName());
-
- /**
- * Background thread used for all Localytics upload processing. This thread is shared across all instances of
- * LocalyticsSession within a process.
- */
- /*
- * By using the class name for the HandlerThread, obfuscation through Proguard is more effective: if Proguard changes the
- * class name, the thread name also changes.
- */
- private static final HandlerThread sUploadHandlerThread = getHandlerThread(UploadHandler.class.getSimpleName());
-
- /**
- * Helper to obtain a new {@link HandlerThread}.
- *
- * @param name to give to the HandlerThread. Useful for debugging, as the thread name is shown in DDMS.
- * @return HandlerThread whose {@link HandlerThread#start()} method has already been called.
- */
- private static HandlerThread getHandlerThread(final String name)
- {
- final HandlerThread thread = new HandlerThread(name, android.os.Process.THREAD_PRIORITY_BACKGROUND);
-
- thread.start();
-
- /*
- * The exception handler needs to be set after start() is called. If it is set before, sometime's the HandlerThread's
- * looper is null. This appears to be a bug in Android.
- */
- thread.setUncaughtExceptionHandler(new ExceptionHandler());
-
- return thread;
- }
-
- /**
- * Handler object where all session requests of this instance of LocalyticsSession are handed off to.
- *
- * This Handler is the key thread synchronization point for all work inside the LocalyticsSession.
- *
- * This handler runs on {@link #sSessionHandlerThread}.
- */
- private final Handler mSessionHandler;
-
- /**
- * Application context
- */
- private final Context mContext;
-
- /**
- * Localytics application key
- */
- private final String mLocalyticsKey;
-
- /**
- * Keeps track of which Localytics clients are currently uploading, in order to allow only one upload for a given key at a
- * time.
- *
- * This field can only be read/written to from the {@link #sSessionHandlerThread}. This invariant is maintained by only
- * accessing this field from within the {@link #mSessionHandler}.
- */
- private static Map
- * If for any reason this is called more than once without an intervening call to {@link #close()}, subsequent calls to open
- * will be ignored.
- *
- * For applications with multiple Activities, every Activity should call
- * This message must be sent before any other messages.
- */
- public static final int MESSAGE_INIT = 0;
-
- /**
- * Empty handler message to open a localytics session
- */
- public static final int MESSAGE_OPEN = 1;
-
- /**
- * Empty handler message to close a localytics session
- */
- public static final int MESSAGE_CLOSE = 2;
-
- /**
- * Handler message to tag an event.
- *
- * {@link Message#obj} is a {@link Pair} instance. This object cannot be null.
- */
- public static final int MESSAGE_TAG_EVENT = 3;
-
- /**
- * Handler message to upload all data collected so far
- *
- * {@link Message#obj} is a {@code Runnable} to execute when upload is complete. The thread that this runnable will
- * executed on is undefined.
- */
- public static final int MESSAGE_UPLOAD = 4;
-
- /**
- * Empty Handler message indicating that a previous upload attempt was completed.
- */
- public static final int MESSAGE_UPLOAD_COMPLETE = 5;
-
- /**
- * Handler message indicating an opt-out choice.
- *
- * {@link Message#arg1} == 1 for true (opt out). 0 means opt-in.
- */
- public static final int MESSAGE_OPT_OUT = 6;
-
- /**
- * Handler message indicating a tag screen event
- *
- * {@link Message#obj} is a string representing the screen visited.
- */
- 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 handler runs on {@link #sUploadHandlerThread}.
- */
- private Handler mUploadHandler;
-
- /**
- * Constructs a new Handler that runs on the given looper.
- *
- * @param context The context used to access resources on behalf of the app. It is recommended to use
- * {@link Context#getApplicationContext()} to avoid the potential memory leak incurred by maintaining
- * references to {@code Activity} instances. Cannot be null.
- * @param key The key unique for each application generated at www.localytics.com. Cannot be null or empty.
- * @param looper to run the Handler on. Cannot be null.
- * @throws IllegalArgumentException if {@code context} is null
- * @throws IllegalArgumentException if {@code key} is null or empty
- */
- public SessionHandler(final Context context, final String key, final Looper looper)
- {
- super(looper);
-
- if (Constants.ENABLE_PARAMETER_CHECKING)
- {
- if (context == null)
- {
- throw new IllegalArgumentException("context cannot be null"); //$NON-NLS-1$
- }
- if (TextUtils.isEmpty(key))
- {
- throw new IllegalArgumentException("key cannot be null or empty"); //$NON-NLS-1$
- }
- }
-
- mContext = context;
- mApiKey = key;
- }
-
- @Override
- public void handleMessage(final Message msg)
- {
- switch (msg.what)
- {
- case MESSAGE_INIT:
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, "Handler received MESSAGE_INIT"); //$NON-NLS-1$
- }
-
- init();
-
- break;
- }
- case MESSAGE_OPT_OUT:
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, "Handler received MESSAGE_OPT_OUT"); //$NON-NLS-1$
- }
-
- final boolean isOptingOut = msg.arg1 == 0 ? false : true;
-
- SessionHandler.this.optOut(isOptingOut);
-
- break;
- }
- case MESSAGE_OPEN:
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, "Handler received MESSAGE_OPEN"); //$NON-NLS-1$
- }
-
- SessionHandler.this.open(false);
-
- break;
- }
- case MESSAGE_CLOSE:
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.d(Constants.LOG_TAG, "Handler received MESSAGE_CLOSE"); //$NON-NLS-1$
- }
-
- SessionHandler.this.close();
-
- break;
- }
- case MESSAGE_TAG_EVENT:
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.d(Constants.LOG_TAG, "Handler received MESSAGE_TAG"); //$NON-NLS-1$
- }
-
- @SuppressWarnings("unchecked")
- final Pair
- * This method must only be called once.
- *
- * 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_INIT} to the Handler.
- *
- * @see #MESSAGE_INIT
- */
- public void init()
- {
- mProvider = LocalyticsProvider.getInstance(mContext, mApiKey);
-
- /*
- * Check whether this session key is opted out
- */
- Cursor cursor = null;
- try
- {
- cursor = mProvider.query(ApiKeysDbColumns.TABLE_NAME, new String[]
- {
- ApiKeysDbColumns._ID,
- ApiKeysDbColumns.OPT_OUT }, String.format("%s = ?", ApiKeysDbColumns.API_KEY), new String[] //$NON-NLS-1$
- { mApiKey }, null);
-
- if (cursor.moveToFirst())
- {
- // API key was previously created
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, String.format("Loading details for API key %s", mApiKey)); //$NON-NLS-1$
- }
-
- mApiKeyId = cursor.getLong(cursor.getColumnIndexOrThrow(ApiKeysDbColumns._ID));
- mIsOptedOut = cursor.getInt(cursor.getColumnIndexOrThrow(ApiKeysDbColumns.OPT_OUT)) != 0;
- }
- else
- {
- // perform first-time initialization of API key
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, String.format("Performing first-time initialization for new API key %s", mApiKey)); //$NON-NLS-1$
- }
-
- final ContentValues values = new ContentValues();
- values.put(ApiKeysDbColumns.API_KEY, mApiKey);
- values.put(ApiKeysDbColumns.UUID, UUID.randomUUID().toString());
- values.put(ApiKeysDbColumns.OPT_OUT, Boolean.FALSE);
- values.put(ApiKeysDbColumns.CREATED_TIME, Long.valueOf(System.currentTimeMillis()));
-
- mApiKeyId = mProvider.insert(ApiKeysDbColumns.TABLE_NAME, values);
- }
- }
- finally
- {
- if (cursor != null)
- {
- cursor.close();
- cursor = null;
- }
- }
-
- if (!sIsUploadingMap.containsKey(mApiKey))
- {
- sIsUploadingMap.put(mApiKey, Boolean.FALSE);
- }
-
- /*
- * Perform lazy initialization of the UploadHandler
- */
- mUploadHandler = new UploadHandler(mContext, this, mApiKey, sUploadHandlerThread.getLooper());
- }
-
- /**
- * Set the opt-in/out-out state for all sessions using the current API key.
- *
- * This method must only be called after {@link #init()} is called.
- *
- * 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_OPT_OUT} to the Handler.
- *
- * @param isOptingOut true if the user is opting out. False if the user is opting back in.
- * @see #MESSAGE_OPT_OUT
- */
- /* package */void optOut(final boolean isOptingOut)
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, String.format("Prior opt-out state is %b, requested opt-out state is %b", Boolean.valueOf(mIsOptedOut), Boolean.valueOf(isOptingOut))); //$NON-NLS-1$
- }
-
- // Do nothing if opt-out is unchanged
- if (mIsOptedOut == isOptingOut)
- {
- return;
- }
-
- mProvider.runBatchTransaction(new Runnable()
- {
- @Override
- public void run()
- {
- final ContentValues values = new ContentValues();
- values.put(ApiKeysDbColumns.OPT_OUT, Boolean.valueOf(isOptingOut));
- mProvider.update(ApiKeysDbColumns.TABLE_NAME, values, String.format("%s = ?", ApiKeysDbColumns._ID), new String[] { Long.toString(mApiKeyId) }); //$NON-NLS-1$
-
- 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
- * on-disk representation, just in case the database update fails.
- */
- mIsOptedOut = isOptingOut;
- }
-
- /**
- * Open a session. While this method should only be called once without an intervening call to {@link #close()}, nothing
- * bad will happen if it is called multiple times.
- *
- * This method must only be called after {@link #init()} is called.
- *
- * 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
- */
- /* package */void open(final boolean ignoreLimits)
- {
- if (mIsSessionOpen)
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.w(Constants.LOG_TAG, "Session was already open"); //$NON-NLS-1$
- }
-
- return;
- }
-
- if (mIsOptedOut)
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.d(Constants.LOG_TAG, "Data collection is opted out"); //$NON-NLS-1$
- }
- return;
- }
-
- /*
- * 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
-
- {
- Cursor eventsCursor = null;
- Cursor blob_eventsCursor = null;
- try
- {
- eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
- { EventsDbColumns._ID }, String.format("%s = ? AND %s >= ?", EventsDbColumns.EVENT_NAME, EventsDbColumns.WALL_TIME), new String[] { CLOSE_EVENT, Long.toString(System.currentTimeMillis() - Constants.SESSION_EXPIRATION) }, EVENTS_SORT_ORDER); //$NON-NLS-1$
- blob_eventsCursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[]
- { 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[]
- { EventsDbColumns._ID }, blob_eventsCursor, new String[]
- { UploadBlobEventsDbColumns.EVENTS_KEY_REF });
-
- for (final CursorJoiner.Result joinerResult : joiner)
- {
- switch (joinerResult)
- {
- case LEFT:
- {
-
- if (-1 != closeEventId)
- {
- /*
- * This should never happen
- */
- if (Constants.IS_LOGGABLE)
- {
- Log.w(Constants.LOG_TAG, "There were multiple close events within SESSION_EXPIRATION"); //$NON-NLS-1$
- }
-
- long newClose = eventsCursor.getLong(eventsCursor.getColumnIndexOrThrow(EventsDbColumns._ID));
- if (newClose > closeEventId)
- {
- closeEventId = newClose;
- }
- }
-
- if (-1 == closeEventId)
- {
- closeEventId = eventsCursor.getLong(idColumn);
- }
-
- break;
- }
- case BOTH:
- break;
- case RIGHT:
- break;
- }
- }
- /*
- * Verify that the session hasn't already been flagged for upload. That could happen if
- */
- }
- finally
- {
- if (eventsCursor != null)
- {
- eventsCursor.close();
- }
- if (blob_eventsCursor != null)
- {
- blob_eventsCursor.close();
- }
- }
- }
-
- if (-1 != closeEventId)
- {
- Log.v(Constants.LOG_TAG, "Opening old closed session and reconnecting"); //$NON-NLS-1$
- mIsSessionOpen = true;
-
- openClosedSession(closeEventId);
- }
- else
- {
- Cursor sessionsCursor = null;
- try
- {
- sessionsCursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[]
- {
- SessionsDbColumns._ID,
- SessionsDbColumns.SESSION_START_WALL_TIME }, null, null, SessionsDbColumns._ID);
-
- if (sessionsCursor.moveToLast())
- {
- if (sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns.SESSION_START_WALL_TIME)) >= System.currentTimeMillis()
- - Constants.SESSION_EXPIRATION)
- {
- // reconnect
- Log.v(Constants.LOG_TAG, "Opening old unclosed session and reconnecting"); //$NON-NLS-1$
- mIsSessionOpen = true;
- mSessionId = sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns._ID));
- return;
- }
-
- // delete empties
- Cursor eventsCursor = null;
- try
- {
- String sessionId = Long.toString(sessionsCursor.getLong(sessionsCursor.getColumnIndexOrThrow(SessionsDbColumns._ID)));
- eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
- { EventsDbColumns._ID }, String.format("%s = ?", EventsDbColumns.SESSION_KEY_REF), new String[] //$NON-NLS-1$
- { sessionId }, null);
-
- if (eventsCursor.getCount() == 0)
- {
- mProvider.delete(SessionsDbColumns.TABLE_NAME, String.format("%s = ?", SessionsDbColumns._ID), new String[] { sessionId }); //$NON-NLS-1$
- }
- }
- finally
- {
- if (null != eventsCursor)
- {
- eventsCursor.close();
- eventsCursor = null;
- }
- }
- }
- }
- finally
- {
- if (null != sessionsCursor)
- {
- sessionsCursor.close();
- sessionsCursor = null;
- }
- }
-
- /*
- * Check that the maximum number of sessions hasn't been exceeded
- */
- if (!ignoreLimits)
- {
- Cursor cursor = null;
- try
- {
- cursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[]
- { SessionsDbColumns._ID }, null, null, null);
-
- if (cursor.getCount() >= Constants.MAX_NUM_SESSIONS)
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.w(Constants.LOG_TAG, "Maximum number of sessions are already on disk--not writing any new sessions until old sessions are cleared out. Try calling upload() to store more sessions."); //$NON-NLS-1$
- }
- return;
- }
- }
- finally
- {
- if (cursor != null)
- {
- cursor.close();
- cursor = null;
- }
- }
- }
-
- Log.v(Constants.LOG_TAG, "Opening new session"); //$NON-NLS-1$
- mIsSessionOpen = true;
-
- openNewSession();
- }
- }
-
- /**
- * 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);
-
- 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);
-
- // 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
- {
- if (null != cursor)
- {
- cursor.close();
- cursor = 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.
- */
- LocalyticsProvider.deleteOldFiles(mContext);
- }
-
- /**
- * Reopens a previous session. This is a helper method to {@link #open(boolean)}.
- *
- * @param closeEventId The last close event which is to be deleted so that the old session can be reopened
- * @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 openClosedSession(final long closeEventId)
- {
- Cursor eventCursor = null;
- try
- {
- eventCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
- { EventsDbColumns.SESSION_KEY_REF }, String.format("%s = ?", EventsDbColumns._ID), new String[] { Long.toString(closeEventId) }, null); //$NON-NLS-1$
-
- if (eventCursor.moveToFirst())
- {
- mSessionId = eventCursor.getLong(eventCursor.getColumnIndexOrThrow(EventsDbColumns.SESSION_KEY_REF));
-
- mProvider.delete(EventsDbColumns.TABLE_NAME, String.format("%s = ?", EventsDbColumns._ID), new String[] { Long.toString(closeEventId) }); //$NON-NLS-1$
- }
- else
- {
- /*
- * This should never happen
- */
-
- if (Constants.IS_LOGGABLE)
- {
- Log.e(Constants.LOG_TAG, "Event no longer exists"); //$NON-NLS-1$
- }
-
- openNewSession();
- }
- }
- finally
- {
- if (eventCursor != null)
- {
- eventCursor.close();
- }
- }
- }
-
- /**
- * Close a session. While this method should only be called after {@link #open(boolean)}, nothing bad will happen if it is
- * called and {@link #open(boolean)} wasn't called. Similarly, nothing bad will happen if close is called multiple times.
- *
- * This method must only be called after {@link #init()} is called.
- *
- * 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
- */
- /* package */void close()
- {
- if (!mIsSessionOpen) // do nothing if session is not open
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.w(Constants.LOG_TAG, "Session was not open, so close is not possible."); //$NON-NLS-1$
- }
- return;
- }
-
- tagEvent(CLOSE_EVENT, null);
-
- mIsSessionOpen = false;
- }
-
- /**
- * Tag an event in a session. While this method shouldn't be called unless {@link #open(boolean)} is called first, this
- * method will simply do nothing if {@link #open(boolean)} hasn't been called.
- *
- * This method must only be called after {@link #init()} is called.
- *
- * 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
- */
- /* package */void tagEvent(final String event, final Map
- * This method performs duplicate suppression, preventing multiple screens with the same value in a row within a given
- * session.
- *
- * 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_SCREEN} to the Handler.
- *
- * @param screen The name of the screen which occurred. Cannot be null or empty.
- * @see #MESSAGE_TAG_SCREEN
- */
- /* package */void tagScreen(final String screen)
- {
- if (!mIsSessionOpen)
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.w(Constants.LOG_TAG, "Tag not written because the session was not open"); //$NON-NLS-1$
- }
- return;
- }
-
- /*
- * Do duplicate suppression
- */
- Cursor cursor = null;
- try
- {
- cursor = mProvider.query(EventHistoryDbColumns.TABLE_NAME, new String[]
- { EventHistoryDbColumns.NAME }, String.format("%s = ? AND %s = ?", EventHistoryDbColumns.TYPE, EventHistoryDbColumns.SESSION_KEY_REF), new String[] { Integer.toString(EventHistoryDbColumns.TYPE_SCREEN), Long.toString(mSessionId) }, String.format("%s DESC", EventHistoryDbColumns._ID)); //$NON-NLS-1$ //$NON-NLS-2$
-
- if (cursor.moveToFirst())
- {
- if (screen.equals(cursor.getString(cursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME))))
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.v(Constants.LOG_TAG, String.format("Suppressed duplicate screen %s", screen)); //$NON-NLS-1$
- }
- return;
- }
- }
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- cursor = null;
- }
- }
-
- /*
- * Write the screen to the database
- */
- final ContentValues values = new ContentValues();
- values.put(EventHistoryDbColumns.NAME, screen);
- values.put(EventHistoryDbColumns.TYPE, Integer.valueOf(EventHistoryDbColumns.TYPE_SCREEN));
- values.put(EventHistoryDbColumns.SESSION_KEY_REF, Long.valueOf(mSessionId));
- values.putNull(EventHistoryDbColumns.PROCESSED_IN_BLOB);
- mProvider.insert(EventHistoryDbColumns.TABLE_NAME, values);
-
- conditionallyAddFlowEvent();
- }
-
- /**
- * Conditionally adds a flow event if no flow event exists in the current upload blob.
- */
- 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
- * occur. A flow event should only be created if there isn't already a flow event that hasn't been associated with an
- * upload blob.
- */
- boolean foundUnassociatedFlowEvent = false;
-
- Cursor eventsCursor = null;
- Cursor blob_eventsCursor = null;
- try
- {
- eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
- { 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, UPLOAD_BLOBS_EVENTS_SORT_ORDER);
-
- final CursorJoiner joiner = new CursorJoiner(eventsCursor, new String[]
- { EventsDbColumns._ID }, blob_eventsCursor, new String[]
- { UploadBlobEventsDbColumns.EVENTS_KEY_REF });
- for (final CursorJoiner.Result joinerResult : joiner)
- {
- switch (joinerResult)
- {
- case LEFT:
- {
- foundUnassociatedFlowEvent = true;
- break;
- }
- case BOTH:
- break;
- case RIGHT:
- break;
- }
- }
- }
- finally
- {
- if (eventsCursor != null)
- {
- eventsCursor.close();
- eventsCursor = null;
- }
-
- if (blob_eventsCursor != null)
- {
- blob_eventsCursor.close();
- blob_eventsCursor = null;
- }
- }
-
- if (!foundUnassociatedFlowEvent)
- {
- tagEvent(FLOW_EVENT, null);
- }
- }
-
- /**
- * Builds upload blobs for all events.
- *
- * @effects Mutates the database by creating a new upload blob for all events that are unassociated at the time this
- * method is called.
- */
- /* 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
- * requires scanning two database tables, the performance won't be a problem for two reasons: 1. This process happens
- * frequently so the number of events to group will always be low. 2. There is a maximum number of events, keeping the
- * overall size low. Note that close events that are younger than SESSION_EXPIRATION will be skipped to allow session
- * reconnects.
- */
-
- // temporary set of event ids that aren't in a blob
- final Set
- * 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 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
- */
- /* package */void upload(final Runnable callback)
- {
- if (sIsUploadingMap.get(mApiKey).booleanValue())
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.d(Constants.LOG_TAG, "Already uploading"); //$NON-NLS-1$
- }
-
- mUploadHandler.sendMessage(mUploadHandler.obtainMessage(UploadHandler.MESSAGE_RETRY_UPLOAD_REQUEST, callback));
- return;
- }
-
- try
- {
- mProvider.runBatchTransaction(new Runnable()
- {
- @Override
- public void run()
- {
- preUploadBuildBlobs();
- }
- });
-
- sIsUploadingMap.put(mApiKey, Boolean.TRUE);
- mUploadHandler.sendMessage(mUploadHandler.obtainMessage(UploadHandler.MESSAGE_UPLOAD, callback));
- }
- catch (final Exception e)
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.w(Constants.LOG_TAG, "Error occurred during upload", e); //$NON-NLS-1$
- }
-
- sIsUploadingMap.put(mApiKey, Boolean.FALSE);
-
- // Notify the caller the upload is "complete"
- if (callback != null)
- {
- /*
- * 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, UploadHandler.UPLOAD_CALLBACK_THREAD_NAME).start();
- }
- }
- }
- }
-
- /**
- * Helper object to the {@link SessionHandler} which helps process upload requests.
- */
- /* 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.
- */
- private final static String ANALYTICS_URL = "http://analytics.localytics.com/api/v2/applications/%s/uploads"; //$NON-NLS-1$
-
- /**
- * Handler message to upload all data collected so far
- *
- * {@link Message#obj} is a {@code Runnable} to execute when upload is complete. The thread that this runnable will
- * executed on is undefined.
- */
- public static final int MESSAGE_UPLOAD = 1;
-
- /**
- * Handler message indicating that there is a queued upload request. When this message is processed, this handler simply
- * forwards the request back to {@link LocalyticsSession#mSessionHandler} with {@link SessionHandler#MESSAGE_UPLOAD}.
- *
- * {@link Message#obj} is a {@code Runnable} to execute when upload is complete. The thread that this runnable will
- * executed on is undefined.
- */
- public static final int MESSAGE_RETRY_UPLOAD_REQUEST = 2;
-
- /**
- * Reference to the Localytics database
- */
- private final LocalyticsProvider mProvider;
-
- /**
- * Application context
- */
- private final Context mContext;
-
- /**
- * The Localytics API key
- */
- private final String mApiKey;
-
- /**
- * Parent session handler to notify when an upload completes.
- */
- private final Handler mSessionHandler;
-
- /**
- * Constructs a new Handler that runs on {@code looper}.
- *
- * Note: This constructor may perform disk access.
- *
- * @param context Application context. Cannot be null.
- * @param sessionHandler Parent {@link SessionHandler} object to notify when uploads are completed. Cannot be null.
- * @param apiKey Localytics API key. Cannot be null.
- * @param looper to run the Handler on. Cannot be null.
- */
- public UploadHandler(final Context context, final Handler sessionHandler, final String apiKey, final Looper looper)
- {
- super(looper);
-
- mContext = context;
- mProvider = LocalyticsProvider.getInstance(context, apiKey);
- mSessionHandler = sessionHandler;
- mApiKey = apiKey;
- }
-
- @Override
- public void handleMessage(final Message msg)
- {
- super.handleMessage(msg);
-
- switch (msg.what)
- {
- case MESSAGE_UPLOAD:
- {
- if (Constants.IS_LOGGABLE)
- {
- Log.d(Constants.LOG_TAG, "UploadHandler Received MESSAGE_UPLOAD"); //$NON-NLS-1$
- }
-
- /*
- * Note that callback may be null
- */
- final Runnable callback = (Runnable) msg.obj;
-
- try
- {
- final List
- * This should be called after a successful upload completes.
- *
- * @param provider Localytics database provider. Cannot be null.
- */
- /* package */static void deleteBlobsAndSessions(final LocalyticsProvider provider)
- {
- /*
- * Deletion needs to occur in a specific order due to database constraints. Specifically, blobevents need to be
- * deleted first. Then blobs themselves can be deleted. Then attributes need to be deleted first. Then events. Then
- * sessions.
- */
-
- final LinkedList
- * There are three types of events: open, close, and application. Open and close events are Localytics events, while
- * application events are generated by the app. The return value of this method will vary based on the type of event that
- * is being converted.
- *
- * @param provider Localytics database instance. Cannot be null.
- * @param context Application context. Cannot be null.
- * @param eventId {@link EventsDbColumns#_ID} of the event to convert.
- * @param blobId {@link UploadBlobEventsDbColumns#_ID} of the upload blob that contains this event.
- * @param apiKey the Localytics API key. Cannot be null.
- * @return JSON representation of the event.
- * @throws JSONException if a problem occurred converting the element to JSON.
- */
- /* package */static JSONObject convertEventToJson(final LocalyticsProvider provider, final Context context, final long eventId, final long blobId, final String apiKey)
- throws JSONException
- {
- final JSONObject result = new JSONObject();
-
- Cursor cursor = null;
-
- try
- {
- cursor = provider.query(EventsDbColumns.TABLE_NAME, null, String.format("%s = ?", EventsDbColumns._ID), new String[] //$NON-NLS-1$
- { Long.toString(eventId) }, EventsDbColumns._ID);
-
- if (cursor.moveToFirst())
- {
- final String eventName = cursor.getString(cursor.getColumnIndexOrThrow(EventsDbColumns.EVENT_NAME));
- final long sessionId = getSessionIdForEventId(provider, eventId);
- final String sessionUuid = getSessionUuid(provider, sessionId);
- final long sessionStartTime = getSessionStartTime(provider, sessionId);
-
- if (OPEN_EVENT.equals(eventName))
- {
- result.put(JsonObjects.SessionOpen.KEY_DATA_TYPE, JsonObjects.SessionOpen.VALUE_DATA_TYPE);
- result.put(JsonObjects.SessionOpen.KEY_WALL_TIME_SECONDS, Math.round((double) cursor.getLong(cursor.getColumnIndex(EventsDbColumns.WALL_TIME))
- / DateUtils.SECOND_IN_MILLIS));
- result.put(JsonObjects.SessionOpen.KEY_EVENT_UUID, sessionUuid);
-
- /*
- * Both the database and the web service use 1-based indexing.
- */
- result.put(JsonObjects.SessionOpen.KEY_COUNT, sessionId);
- }
- else if (CLOSE_EVENT.equals(eventName))
- {
- result.put(JsonObjects.SessionClose.KEY_DATA_TYPE, JsonObjects.SessionClose.VALUE_DATA_TYPE);
- result.put(JsonObjects.SessionClose.KEY_EVENT_UUID, cursor.getString(cursor.getColumnIndexOrThrow(EventsDbColumns.UUID)));
- result.put(JsonObjects.SessionClose.KEY_SESSION_UUID, sessionUuid);
- result.put(JsonObjects.SessionClose.KEY_SESSION_START_TIME, Math.round((double) sessionStartTime / DateUtils.SECOND_IN_MILLIS));
- result.put(JsonObjects.SessionClose.KEY_WALL_TIME_SECONDS, Math.round((double) cursor.getLong(cursor.getColumnIndex(EventsDbColumns.WALL_TIME))
- / DateUtils.SECOND_IN_MILLIS));
-
- /*
- * length is a special case, as it depends on the start time embedded in the session table
- */
- Cursor sessionCursor = null;
- try
- {
- sessionCursor = provider.query(SessionsDbColumns.TABLE_NAME, new String[]
- { SessionsDbColumns.SESSION_START_WALL_TIME }, String.format("%s = ?", SessionsDbColumns._ID), new String[] { Long.toString(cursor.getLong(cursor.getColumnIndexOrThrow(EventsDbColumns.SESSION_KEY_REF))) }, null); //$NON-NLS-1$
-
- if (sessionCursor.moveToFirst())
- {
- result.put(JsonObjects.SessionClose.KEY_SESSION_LENGTH_SECONDS, Math.round((double) cursor.getLong(cursor.getColumnIndex(EventsDbColumns.WALL_TIME))
- / DateUtils.SECOND_IN_MILLIS)
- - Math.round((double) sessionCursor.getLong(sessionCursor.getColumnIndexOrThrow(SessionsDbColumns.SESSION_START_WALL_TIME))
- / DateUtils.SECOND_IN_MILLIS));
- }
- else
- {
- // this should never happen
- throw new RuntimeException("Session didn't exist"); //$NON-NLS-1$
- }
- }
- finally
- {
- if (null != sessionCursor)
- {
- sessionCursor.close();
- sessionCursor = null;
- }
- }
-
- /*
- * The close also contains a special case element for the screens history
- */
- Cursor eventHistoryCursor = null;
- try
- {
- eventHistoryCursor = provider.query(EventHistoryDbColumns.TABLE_NAME, new String[]
- { EventHistoryDbColumns.NAME }, String.format("%s = ? AND %s = ?", EventHistoryDbColumns.SESSION_KEY_REF, EventHistoryDbColumns.TYPE), new String[] { Long.toString(sessionId), Integer.toString(EventHistoryDbColumns.TYPE_SCREEN) }, EventHistoryDbColumns._ID); //$NON-NLS-1$
-
- final JSONArray screens = new JSONArray();
- while (eventHistoryCursor.moveToNext())
- {
- screens.put(eventHistoryCursor.getString(eventHistoryCursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME)));
- }
-
- if (screens.length() > 0)
- {
- result.put(JsonObjects.SessionClose.KEY_FLOW_ARRAY, screens);
- }
- }
- finally
- {
- if (null != eventHistoryCursor)
- {
- eventHistoryCursor.close();
- eventHistoryCursor = null;
- }
- }
- }
- else if (OPT_IN_EVENT.equals(eventName) || OPT_OUT_EVENT.equals(eventName))
- {
- result.put(JsonObjects.OptEvent.KEY_DATA_TYPE, JsonObjects.OptEvent.VALUE_DATA_TYPE);
- result.put(JsonObjects.OptEvent.KEY_API_KEY, apiKey);
- result.put(JsonObjects.OptEvent.KEY_OPT, OPT_OUT_EVENT.equals(eventName) ? Boolean.TRUE.toString() : Boolean.FALSE.toString());
- result.put(JsonObjects.OptEvent.KEY_WALL_TIME_SECONDS, Math.round((double) cursor.getLong(cursor.getColumnIndex(EventsDbColumns.WALL_TIME))
- / DateUtils.SECOND_IN_MILLIS));
- }
- else if (FLOW_EVENT.equals(eventName))
- {
- result.put(JsonObjects.EventFlow.KEY_DATA_TYPE, JsonObjects.EventFlow.VALUE_DATA_TYPE);
- result.put(JsonObjects.EventFlow.KEY_EVENT_UUID, cursor.getString(cursor.getColumnIndexOrThrow(EventsDbColumns.UUID)));
- result.put(JsonObjects.EventFlow.KEY_SESSION_START_TIME, Math.round((double) sessionStartTime / DateUtils.SECOND_IN_MILLIS));
-
- /*
- * Need to generate two objects: the old flow events and the new flow events
- */
-
- /*
- * Default sort order is ascending by _ID, so these will be sorted chronologically.
- */
- Cursor eventHistoryCursor = null;
- try
- {
- eventHistoryCursor = provider.query(EventHistoryDbColumns.TABLE_NAME, new String[]
- {
- EventHistoryDbColumns.TYPE,
- EventHistoryDbColumns.PROCESSED_IN_BLOB,
- EventHistoryDbColumns.NAME }, String.format("%s = ? AND %s <= ?", EventHistoryDbColumns.SESSION_KEY_REF, EventHistoryDbColumns.PROCESSED_IN_BLOB), new String[] { Long.toString(sessionId), Long.toString(blobId) }, EventHistoryDbColumns._ID); //$NON-NLS-1$
-
- final JSONArray newScreens = new JSONArray();
- final JSONArray oldScreens = new JSONArray();
- while (eventHistoryCursor.moveToNext())
- {
- final String name = eventHistoryCursor.getString(eventHistoryCursor.getColumnIndexOrThrow(EventHistoryDbColumns.NAME));
- final String type;
- if (EventHistoryDbColumns.TYPE_EVENT == eventHistoryCursor.getInt(eventHistoryCursor.getColumnIndexOrThrow(EventHistoryDbColumns.TYPE)))
- {
- type = JsonObjects.EventFlow.Element.TYPE_EVENT;
- }
- else
- {
- type = JsonObjects.EventFlow.Element.TYPE_SCREEN;
- }
-
- if (blobId == eventHistoryCursor.getLong(eventHistoryCursor.getColumnIndexOrThrow(EventHistoryDbColumns.PROCESSED_IN_BLOB)))
- {
- newScreens.put(new JSONObject().put(type, name));
- }
- else
- {
- oldScreens.put(new JSONObject().put(type, name));
- }
- }
-
- result.put(JsonObjects.EventFlow.KEY_FLOW_NEW, newScreens);
- result.put(JsonObjects.EventFlow.KEY_FLOW_OLD, oldScreens);
- }
- finally
- {
- if (null != eventHistoryCursor)
- {
- eventHistoryCursor.close();
- eventHistoryCursor = null;
- }
- }
- }
- else
- {
- /*
- * This is a normal application event
- */
-
- result.put(JsonObjects.SessionEvent.KEY_DATA_TYPE, JsonObjects.SessionEvent.VALUE_DATA_TYPE);
- result.put(JsonObjects.SessionEvent.KEY_WALL_TIME_SECONDS, Math.round((double) cursor.getLong(cursor.getColumnIndex(EventsDbColumns.WALL_TIME))
- / DateUtils.SECOND_IN_MILLIS));
- result.put(JsonObjects.SessionEvent.KEY_EVENT_UUID, cursor.getString(cursor.getColumnIndexOrThrow(EventsDbColumns.UUID)));
- result.put(JsonObjects.SessionEvent.KEY_SESSION_UUID, sessionUuid);
- result.put(JsonObjects.SessionEvent.KEY_NAME, eventName.substring(context.getPackageName().length() + 1, eventName.length()));
-
- final JSONObject attributes = convertAttributesToJson(provider, eventId);
-
- if (null != attributes)
- {
- result.put(JsonObjects.SessionEvent.KEY_ATTRIBUTES, attributes);
- }
- }
- }
- else
- {
- /*
- * This should never happen
- */
- throw new RuntimeException();
- }
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- }
- }
-
- return result;
- }
-
- /**
- * Private helper to get the {@link SessionsDbColumns#_ID} for a given {@link EventsDbColumns#_ID}.
- *
- * @param provider Localytics database instance. Cannot be null.
- * @param eventId {@link EventsDbColumns#_ID} of the event to look up
- * @return The {@link SessionsDbColumns#_ID} of the session that owns the event.
- */
- /* package */static long getSessionIdForEventId(final LocalyticsProvider provider, final long eventId)
- {
- Cursor cursor = null;
- try
- {
- cursor = provider.query(EventsDbColumns.TABLE_NAME, new String[]
- { EventsDbColumns.SESSION_KEY_REF }, String.format("%s = ?", EventsDbColumns._ID), new String[] { Long.toString(eventId) }, null); //$NON-NLS-1$
-
- if (cursor.moveToFirst())
- {
- return cursor.getLong(cursor.getColumnIndexOrThrow(EventsDbColumns.SESSION_KEY_REF));
- }
-
- /*
- * This should never happen
- */
- throw new RuntimeException();
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- }
- }
- }
-
- /**
- * Private helper to get the {@link SessionsDbColumns#UUID} for a given {@link SessionsDbColumns#_ID}.
- *
- * @param provider Localytics database instance. Cannot be null.
- * @param sessionId {@link SessionsDbColumns#_ID} of the event to look up
- * @return The {@link SessionsDbColumns#UUID} of the session.
- */
- /* package */static String getSessionUuid(final LocalyticsProvider provider, final long sessionId)
- {
- Cursor cursor = null;
- try
- {
- cursor = provider.query(SessionsDbColumns.TABLE_NAME, new String[]
- { SessionsDbColumns.UUID }, String.format("%s = ?", SessionsDbColumns._ID), new String[] { Long.toString(sessionId) }, null); //$NON-NLS-1$
-
- if (cursor.moveToFirst())
- {
- return cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.UUID));
- }
-
- /*
- * This should never happen
- */
- throw new RuntimeException();
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- }
- }
- }
-
- /**
- * Private helper to get the {@link SessionsDbColumns#SESSION_START_WALL_TIME} for a given {@link SessionsDbColumns#_ID}.
- *
- * @param provider Localytics database instance. Cannot be null.
- * @param sessionId {@link SessionsDbColumns#_ID} of the event to look up
- * @return The {@link SessionsDbColumns#SESSION_START_WALL_TIME} of the session.
- */
- /* package */static long getSessionStartTime(final LocalyticsProvider provider, final long sessionId)
- {
- Cursor cursor = null;
- try
- {
- cursor = provider.query(SessionsDbColumns.TABLE_NAME, new String[]
- { SessionsDbColumns.SESSION_START_WALL_TIME }, String.format("%s = ?", SessionsDbColumns._ID), new String[] { Long.toString(sessionId) }, null); //$NON-NLS-1$
-
- if (cursor.moveToFirst())
- {
- return cursor.getLong(cursor.getColumnIndexOrThrow(SessionsDbColumns.SESSION_START_WALL_TIME));
- }
-
- /*
- * This should never happen
- */
- throw new RuntimeException();
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- }
- }
- }
-
- /**
- * Private helper to convert an event's attributes into a {@link JSONObject} representation.
- *
- * @param provider Localytics database instance. Cannot be null.
- * @param eventId {@link EventsDbColumns#_ID} of the event whose attributes are to be loaded.
- * @return {@link JSONObject} representing the attributes of the event. The order of attributes is undefined and may
- * change from call to call of this method. If the event has no attributes, returns null.
- * @throws JSONException if an error occurs converting the attributes to JSON
- */
- /* package */static JSONObject convertAttributesToJson(final LocalyticsProvider provider, final long eventId) throws JSONException
- {
- Cursor cursor = null;
- try
- {
- cursor = provider.query(AttributesDbColumns.TABLE_NAME, null, String.format("%s = ?", AttributesDbColumns.EVENTS_KEY_REF), new String[] { Long.toString(eventId) }, null); //$NON-NLS-1$
-
- if (cursor.getCount() == 0)
- {
- return null;
- }
-
- final JSONObject attributes = new JSONObject();
-
- final int keyColumn = cursor.getColumnIndexOrThrow(AttributesDbColumns.ATTRIBUTE_KEY);
- final int valueColumn = cursor.getColumnIndexOrThrow(AttributesDbColumns.ATTRIBUTE_VALUE);
- while (cursor.moveToNext())
- {
- attributes.put(cursor.getString(keyColumn), cursor.getString(valueColumn));
- }
-
- return attributes;
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- }
- }
- }
-
- /**
- * Given an id of an upload blob, get the session id associated with that blob.
- *
- * @param blobId {@link UploadBlobsDbColumns#_ID} of the upload blob.
- * @return id of the parent session.
- */
- /* package */long getSessionIdForBlobId(final long blobId)
- {
- /*
- * This implementation needs to walk up the tree of database elements.
- */
-
- long eventId;
- {
- Cursor cursor = null;
- try
- {
- cursor = mProvider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[]
- { UploadBlobEventsDbColumns.EVENTS_KEY_REF }, String.format("%s = ?", UploadBlobEventsDbColumns.UPLOAD_BLOBS_KEY_REF), new String[] //$NON-NLS-1$
- { Long.toString(blobId) }, null);
-
- if (cursor.moveToFirst())
- {
- eventId = cursor.getLong(cursor.getColumnIndexOrThrow(UploadBlobEventsDbColumns.EVENTS_KEY_REF));
- }
- else
- {
- /*
- * This should never happen
- */
- throw new RuntimeException("No events associated with blob"); //$NON-NLS-1$
- }
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- }
- }
- }
-
- long sessionId;
- {
- Cursor cursor = null;
- try
- {
- cursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[]
- { EventsDbColumns.SESSION_KEY_REF }, String.format("%s = ?", EventsDbColumns._ID), new String[] //$NON-NLS-1$
- { Long.toString(eventId) }, null);
-
- if (cursor.moveToFirst())
- {
- sessionId = cursor.getLong(cursor.getColumnIndexOrThrow(EventsDbColumns.SESSION_KEY_REF));
- }
- else
- {
- /*
- * This should never happen
- */
- throw new RuntimeException("No session associated with event"); //$NON-NLS-1$
- }
- }
- finally
- {
- if (null != cursor)
- {
- cursor.close();
- }
- }
- }
-
- return sessionId;
- }
- }
-
- /**
- * Internal helper class to pass two objects to the Handler via the {@link Message#obj}.
- */
- /*
- * Once support for Android 1.6 is dropped, using Android's built-in Pair class would be preferable
- */
- private static final class Pair
- * This is not a general-purpose reflection class but is rather specifically designed for calling methods that must exist in newer
- * versions of Android.
- */
-public final class ReflectionUtils
-{
- /**
- * Private constructor prevents instantiation
- *
- * @throws UnsupportedOperationException because this class cannot be instantiated.
- */
- private ReflectionUtils()
- {
- throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
- }
-
- /**
- * Use reflection to invoke a static method for a class object and method name
- *
- * @param
- *
- *
- *
- * Permissions recommended:
- *
- *
- *
- *
- *
- * There are very serious implications to the quality of your data when providing an opt out option. For example, users who
- * have opted out will appear as never returning, causing your new/returning chart to skew.
- * If two instances of the same application are running, and one is opted in and the second opts out, the first will also
- * become opted out, and neither will collect any more data.
- * If a session was started while the app was opted out, the session open event has already been lost. For this reason, all
- * sessions started while opted out will not collect data even after the user opts back in or else it taints the comparisons
- * of session lengths and other metrics.
- *
- * @param isOptedOut True if the user should be be opted out and have all his Localytics data deleted.
- */
- public void setOptOut(final boolean isOptedOut)
- {
- mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_OPT_OUT, isOptedOut ? 1 : 0, 0));
- }
-
- /**
- * Opens the Localytics session. The session time as presented on the website is the time between the first open
- * and the final close
so it is recommended to open the session as early as possible, and close it at the last
- * moment. The session must be opened before {@link #tagEvent(String)} or {@link #tagEvent(String, Map)} can be called, so
- * this call should be placed in {@code Activity#onCreate(Bundle)}.
- * open
in onCreate
. This will
- * cause each Activity to reconnect to the currently running session.
- */
- public void open()
- {
- mSessionHandler.sendEmptyMessage(SessionHandler.MESSAGE_OPEN);
- }
-
- /**
- * Closes the Localytics session. This should be done when the application or activity is ending. Because of the way the
- * Android lifecycle works, this call could end up in a place which gets called multiple times (such as onPause
- * which is the recommended location). This is fine because only the last close is processed by the server.
- * Closing does not cause the session to stop collecting data. This is a result of the application life cycle. It is possible
- * for onPause to be called long before the application is actually ready to close the session.
- */
- public void close()
- {
- mSessionHandler.sendEmptyMessage(SessionHandler.MESSAGE_CLOSE);
- }
-
- /**
- * Allows a session to tag a particular event as having occurred. For example, if a view has three buttons, it might make
- * sense to tag each button click with the name of the button which was clicked. For another example, in a game with many
- * levels it might be valuable to create a new tag every time the user gets to a new level in order to determine how far the
- * average user is progressing in the game.
- * Tagging Best Practices
- *
- *
- *
- *
- * @param event The name of the event which occurred. Cannot be null or empty string.
- * @throws IllegalArgumentException if {@code event} is null.
- * @throws IllegalArgumentException if {@code event} is empty.
- */
- public void tagEvent(final String event)
- {
- tagEvent(event, null);
- }
-
- /**
- * Allows a session to tag a particular event as having occurred, and optionally attach a collection of attributes to it. For
- * example, if a view has three buttons, it might make sense to tag each button with the name of the button which was clicked.
- * For another example, in a game with many levels it might be valuable to create a new tag every time the user gets to a new
- * level in order to determine how far the average user is progressing in the game.
- * Tagging Best Practices
- *
- *
- *
- *
- * @param event The name of the event which occurred.
- * @param attributes The collection of attributes for this particular event. If this parameter is null or empty, then calling
- * this method has the same effect as calling {@link #tagEvent(String)}. This parameter may not contain null or
- * empty keys or values.
- * @throws IllegalArgumentException if {@code event} is null.
- * @throws IllegalArgumentException if {@code event} is empty.
- * @throws IllegalArgumentException if {@code attributes} contains null keys, empty keys, null values, or empty values.
- */
- public void tagEvent(final String event, final Map