From 79313d1b0d550cbfffe6cf4d98f665bb089f6dfa Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 21 Aug 2013 10:26:01 -0500 Subject: [PATCH] Remove analytics * Remove findbugs * Remove localytics * Remove crittercism * Remove anonymous statistics menu entry * Remove coarse location permission --- .idea/libraries/crittercism.xml | 9 - .idea/libraries/findbugs_annotations.xml | 9 - .../com/todoroo/astrid/sync/SyncProvider.java | 1 - .../todoroo/astrid/sync/SyncV2Provider.java | 1 - astrid/AndroidManifest.xml | 7 - astrid/astrid.iml | 2 - .../com/localytics/android/Constants.java | 96 - .../localytics/android/DatapointHelper.java | 392 --- .../localytics/android/ExceptionHandler.java | 42 - .../com/localytics/android/JsonObjects.java | 574 ---- .../android/LocalyticsProvider.java | 1149 ------- .../localytics/android/LocalyticsSession.java | 2765 ----------------- .../localytics/android/ReflectionUtils.java | 114 - astrid/libs/crittercism_v3_0_7_sdkonly.jar | Bin 58125 -> 0 bytes astrid/libs/findbugs-annotations.jar | Bin 14551 -> 0 bytes .../astrid/actfm/ActFmBackgroundService.java | 3 - .../astrid/actfm/ActFmGoogleAuthActivity.java | 4 - .../astrid/actfm/ActFmLoginActivity.java | 9 - .../astrid/actfm/ActFmPreferences.java | 2 - .../astrid/actfm/CommentsFragment.java | 3 - .../astrid/actfm/EditPeopleControlSet.java | 3 - .../astrid/actfm/TagSettingsActivity.java | 2 - .../actfm/sync/ActFmPreferenceService.java | 2 - .../astrid/actfm/sync/ActFmSyncThread.java | 2 - .../actfm/sync/AstridNewSyncMigrator.java | 25 - .../actfm/sync/messages/ChangesHappened.java | 2 - .../sync/messages/ClientToServerMessage.java | 2 - .../astrid/backup/BackupConstants.java | 2 - .../astrid/core/CustomFilterActivity.java | 4 - .../todoroo/astrid/gcal/GCalControlSet.java | 2 - .../gtasks/GtasksBackgroundService.java | 3 - .../gtasks/GtasksPreferenceService.java | 2 - .../gtasks/auth/GtasksLoginActivity.java | 4 - .../gtasks/sync/GtasksSyncV2Provider.java | 2 - .../astrid/locale/LocaleEditAlerts.java | 7 - .../astrid/notes/EditNoteActivity.java | 2 - .../reminders/NotificationFragment.java | 2 - .../astrid/reminders/ReminderDialog.java | 2 - .../astrid/reminders/ReminderPreferences.java | 1 - .../astrid/repeats/RepeatControlSet.java | 2 - .../repeats/RepeatTaskCompleteListener.java | 4 - .../reusable/FeaturedTaskListFragment.java | 2 - .../todoroo/astrid/timers/TimerPlugin.java | 3 - astrid/proguard.cfg | 6 - astrid/res/values/keys.xml | 5 - astrid/res/xml/preferences_misc.xml | 6 - .../astrid/activity/AstridActivity.java | 6 - .../astrid/activity/EditPreferences.java | 32 - .../src/com/todoroo/astrid/activity/Eula.java | 2 - .../astrid/activity/FilterListFragment.java | 4 - .../activity/FilterShortcutActivity.java | 4 - .../astrid/activity/ShareActivity.java | 3 - .../astrid/activity/TaskEditFragment.java | 7 - .../astrid/activity/TaskListActivity.java | 7 - .../astrid/activity/TaskListFragment.java | 6 - .../todoroo/astrid/adapter/AddOnAdapter.java | 2 - .../src/com/todoroo/astrid/dao/Database.java | 3 - .../com/todoroo/astrid/dao/MetadataDao.java | 3 - .../todoroo/astrid/dao/StoreObjectDao.java | 1 - .../com/todoroo/astrid/dao/TagDataDao.java | 1 - .../todoroo/astrid/dao/TagMetadataDao.java | 1 - .../todoroo/astrid/dao/TaskAttachmentDao.java | 1 - .../src/com/todoroo/astrid/dao/TaskDao.java | 1 - .../astrid/dao/TaskListMetadataDao.java | 1 - .../src/com/todoroo/astrid/dao/UpdateDao.java | 1 - .../src/com/todoroo/astrid/dao/UserDao.java | 1 - .../astrid/provider/Astrid2TaskProvider.java | 2 - .../astrid/service/StartupService.java | 11 - .../astrid/service/StatisticsConstants.java | 1 - .../astrid/service/StatisticsService.java | 113 - .../todoroo/astrid/service/TaskService.java | 3 - .../ABTestEventReportingService.java | 45 - .../service/abtesting/ABTestInvoker.java | 16 - .../com/todoroo/astrid/ui/QuickAddBar.java | 2 - .../astrid/ui/RandomReminderControlSet.java | 2 - .../com/todoroo/astrid/utility/Constants.java | 10 - .../welcome/tutorial/WelcomeWalkthrough.java | 3 - .../astrid/widget/WidgetConfigActivity.java | 6 - 78 files changed, 5582 deletions(-) delete mode 100644 .idea/libraries/crittercism.xml delete mode 100644 .idea/libraries/findbugs_annotations.xml delete mode 100644 astrid/common-src/com/localytics/android/Constants.java delete mode 100755 astrid/common-src/com/localytics/android/DatapointHelper.java delete mode 100644 astrid/common-src/com/localytics/android/ExceptionHandler.java delete mode 100644 astrid/common-src/com/localytics/android/JsonObjects.java delete mode 100644 astrid/common-src/com/localytics/android/LocalyticsProvider.java delete mode 100755 astrid/common-src/com/localytics/android/LocalyticsSession.java delete mode 100644 astrid/common-src/com/localytics/android/ReflectionUtils.java delete mode 100644 astrid/libs/crittercism_v3_0_7_sdkonly.jar delete mode 100644 astrid/libs/findbugs-annotations.jar delete mode 100644 astrid/src/com/todoroo/astrid/service/StatisticsService.java diff --git a/.idea/libraries/crittercism.xml b/.idea/libraries/crittercism.xml deleted file mode 100644 index 7adc26c0e..000000000 --- a/.idea/libraries/crittercism.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/findbugs_annotations.xml b/.idea/libraries/findbugs_annotations.xml deleted file mode 100644 index 925fee97b..000000000 --- a/.idea/libraries/findbugs_annotations.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/api/src/com/todoroo/astrid/sync/SyncProvider.java b/api/src/com/todoroo/astrid/sync/SyncProvider.java index 60fc82ccf..93ffd44ef 100644 --- a/api/src/com/todoroo/astrid/sync/SyncProvider.java +++ b/api/src/com/todoroo/astrid/sync/SyncProvider.java @@ -389,7 +389,6 @@ public abstract class SyncProvider { * whether to display a dialog */ protected void handleException(String tag, Exception e, boolean displayError) { - //TODO: When Crittercism supports it, report error to them final Context context = ContextManager.getContext(); getUtilities().setLastError(e.toString(), ""); diff --git a/api/src/com/todoroo/astrid/sync/SyncV2Provider.java b/api/src/com/todoroo/astrid/sync/SyncV2Provider.java index 2d3c7d059..0eeaa8f5d 100644 --- a/api/src/com/todoroo/astrid/sync/SyncV2Provider.java +++ b/api/src/com/todoroo/astrid/sync/SyncV2Provider.java @@ -18,7 +18,6 @@ abstract public class SyncV2Provider { public class SyncExceptionHandler { public void handleException(String tag, Exception e, String type) { - //TODO: When Crittercism supports it, report error to them getUtilities().setLastError(e.toString(), type); // occurs when application was closed diff --git a/astrid/AndroidManifest.xml b/astrid/AndroidManifest.xml index 69d8e9d39..97148bc03 100644 --- a/astrid/AndroidManifest.xml +++ b/astrid/AndroidManifest.xml @@ -26,8 +26,6 @@ - - @@ -191,11 +189,6 @@ android:screenOrientation="portrait" android:theme="@android:style/Theme" /> - - - - - diff --git a/astrid/astrid.iml b/astrid/astrid.iml index 5d745536e..a4288446f 100644 --- a/astrid/astrid.iml +++ b/astrid/astrid.iml @@ -41,8 +41,6 @@ - - diff --git a/astrid/common-src/com/localytics/android/Constants.java b/astrid/common-src/com/localytics/android/Constants.java deleted file mode 100644 index 087cc19fa..000000000 --- a/astrid/common-src/com/localytics/android/Constants.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2012 Todoroo Inc - * - * See the file "LICENSE" for the full license governing this code. - */ -package com.localytics.android; - -import android.text.format.DateUtils; - -/** - * Build constants for the Localytics library. - *

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

    - *
  1. 1: Initial version
  2. - *
  3. 2: No format changes--just deleting bad data stranded in the database
  4. - *
- */ - private static final int DATABASE_VERSION = 2; - - /** - * Singleton instance of the {@link LocalyticsProvider}. Lazily initialized via {@link #getInstance(Context, String)}. - */ - private static final Map sLocalyticsProviderMap = new HashMap(); - - /** - * Intrinsic lock for synchronizing the initialization of {@link #sLocalyticsProviderMap}. - */ - /* - * Fun fact: Object[0] is more efficient that Object for an intrinsic lock - */ - private static final Object[] sLocalyticsProviderIntrinsicLock = new Object[0]; - - /** - * Unmodifiable set of valid table names. - */ - private static final Set sValidTables = Collections.unmodifiableSet(getValidTables()); - - /** - * SQLite database owned by the provider. - */ - private final SQLiteDatabase mDb; - - /** - * Obtains an instance of the Localytics Provider. Since the provider is a singleton object, only a single instance will be - * returned. - *

- * 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 getValidTables() - { - final HashSet tables = new HashSet(); - - tables.add(ApiKeysDbColumns.TABLE_NAME); - tables.add(AttributesDbColumns.TABLE_NAME); - tables.add(EventsDbColumns.TABLE_NAME); - tables.add(EventHistoryDbColumns.TABLE_NAME); - tables.add(SessionsDbColumns.TABLE_NAME); - tables.add(UploadBlobsDbColumns.TABLE_NAME); - tables.add(UploadBlobEventsDbColumns.TABLE_NAME); - - return tables; - } - - /** - * Private helper that deletes files from older versions of the Localytics library. - *

- * Note: This is a private method that is only made package-accessible for unit testing. - * - * @param context application context - * @throws IllegalArgumentException if {@code context} is null - */ - /* 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: - *

    - *
  • {@link permission#INTERNET}
  • - Necessary to upload data to the webservice. - *
- * Permissions recommended: - *
    - *
  • {@link permission#ACCESS_WIFI_STATE}
  • - Without this users connecting via Wi-Fi will show up as having a connection - * type of 'unknown' on the webservice - *
- *

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

- * Best Practices - *
    - *
  • Instantiate and open a {@link LocalyticsSession} object in {@code Activity#onCreate(Bundle)}. This will cause every new - * Activity displayed to reconnect to any running session.
  • - *
  • Consider also performing {@link #upload()} in {@code Activity#onCreate(Bundle)}. This makes it more likely for the upload - * to complete before the Activity is finished, and also causes the upload to start before the user has a chance to begin any data - * intensive actions of his own.
  • - *
  • Close the session in {@code Activity#onPause()}. Based on the Activity lifecycle documentation, this is the last - * terminating method which is guaranteed to be called. The final call to {@link #close()} is the only one considered, so don't - * worry about Activity re-entrance.
  • - *
  • Do not call any {@link LocalyticsSession} methods inside a loop. Instead, calls such as {@link #tagEvent(String)} should - * follow user actions. This limits the amount of data which is stored and uploaded.
  • - *
- *

- * 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 sIsUploadingMap = new HashMap(); - - /** - * Constructs a new {@link LocalyticsSession} object. - * - * @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. - * @throws IllegalArgumentException if {@code context} is null - * @throws IllegalArgumentException if {@code key} is null or empty - */ - public LocalyticsSession(final Context context, final String key) - { - 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$ - } - - /* - * Get the application context to avoid having the Localytics object holding onto an Activity object. Using application - * context is very important to prevent the customer from giving the library multiple different contexts with different - * package names, which would corrupt the events in the database. - * - * 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. - * - * Note that getting the application context may have unpredictable results for apps sharing a process running Android 2.1 - * and earlier. See for details. - */ - mContext = !(context.getClass().getName().equals("android.test.RenamingDelegatingContext")) && Constants.CURRENT_API_LEVEL >= 8 ? context.getApplicationContext() : context; //$NON-NLS-1$ - mLocalyticsKey = key; - - mSessionHandler = new SessionHandler(mContext, mLocalyticsKey, sSessionHandlerThread.getLooper()); - - /* - * Complete Handler initialization on a background thread. Note that this is not generally a good best practice, as the - * LocalyticsSession object (and its child objects) should be fully initialized by the time the constructor returns. - * However this implementation is safe, as the Handler will process this initialization message before any other message. - */ - mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_INIT)); - } - - /** - * Sets the Localytics opt-out state for this application. This call is not necessary and is provided for people who wish to - * allow their users the ability to opt out of data collection. It can be called at any time. Passing true causes all further - * data collection to stop, and an opt-out event to be sent to the server so the user's data is removed from the charts.
- * 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)}. - *

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

    - *
  • DO NOT use tags to record personally identifiable information.
  • - *
  • The best way to use tags is to create all the tag strings as predefined constants and only use those. This is more - * efficient and removes the risk of collecting personal information.
  • - *
  • Do not set tags inside loops or any other place which gets called frequently. This can cause a lot of data to be stored - * and uploaded.
  • - *
- *
- * - * @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 - *
    - *
  • DO NOT use tags to record personally identifiable information.
  • - *
  • The best way to use tags is to create all the tag strings as predefined constants and only use those. This is more - * efficient and removes the risk of collecting personal information.
  • - *
  • Do not set tags inside loops or any other place which gets called frequently. This can cause a lot of data to be stored - * and uploaded.
  • - *
- *
- * - * @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 attributes) - { - if (Constants.ENABLE_PARAMETER_CHECKING) - { - if (null == event) - { - throw new IllegalArgumentException("event cannot be null"); //$NON-NLS-1$ - } - - if (0 == event.length()) - { - throw new IllegalArgumentException("event cannot be empty"); //$NON-NLS-1$ - } - - if (null != attributes) - { - /* - * Calling this with empty attributes is a smell that indicates a possible programming error on the part of the - * caller - */ - if (attributes.isEmpty()) - { - if (Constants.IS_LOGGABLE) - { - Log.i(Constants.LOG_TAG, "attributes is empty. Did the caller make an error?"); //$NON-NLS-1$ - } - } - - for (final Entry entry : attributes.entrySet()) - { - final String key = entry.getKey(); - final String value = entry.getValue(); - - if (null == key) - { - throw new IllegalArgumentException("attributes cannot contain null keys"); //$NON-NLS-1$ - } - if (null == value) - { - throw new IllegalArgumentException("attributes cannot contain null values"); //$NON-NLS-1$ - } - if (0 == key.length()) - { - throw new IllegalArgumentException("attributes cannot contain empty keys"); //$NON-NLS-1$ - } - if (0 == value.length()) - { - throw new IllegalArgumentException("attributes cannot contain empty values"); //$NON-NLS-1$ - } - } - } - } - - final String eventString = String.format(EVENT_FORMAT, mContext.getPackageName(), event); - - if (null == attributes) - { - mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_TAG_EVENT, new Pair>(eventString, null))); - } - else - { - /* - * Note: it is important to make a copy of the map, to ensure that a client can't modify the map after this method is - * called. A TreeMap is used to ensure that the order that the attributes are written is deterministic. For example, - * if the maximum number of attributes is exceeded the entries that occur later alphabetically will be skipped - * consistently. - */ - mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_TAG_EVENT, new Pair>( - eventString, - new TreeMap( - attributes)))); - } - } - - /** - * Note: This implementation will perform duplicate suppression on two identical screen events that occur in a row within a - * single session. For example, in the set of screens {"Screen 1", "Screen 1"} the second screen would be suppressed. However - * in the set {"Screen 1", "Screen 2", "Screen 1"}, no duplicate suppression would occur. - * - * @param screen Name of the screen that was entered. Cannot be null or the empty string. - * @throws IllegalArgumentException if {@code event} is null. - * @throws IllegalArgumentException if {@code event} is empty. - */ - public void tagScreen(final String screen) - { - if (null == screen) - { - throw new IllegalArgumentException("event cannot be null"); //$NON-NLS-1$ - } - - if (0 == screen.length()) - { - throw new IllegalArgumentException("event cannot be empty"); //$NON-NLS-1$ - } - - mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_TAG_SCREEN, screen)); - } - - /** - * Initiates an upload of any Localytics data for this session's API key. This should be done early in the process life in - * order to guarantee as much time as possible for slow connections to complete. It is necessary to do this even if the user - * has opted out because this is how the opt out is transported to the webservice. - */ - public void upload() - { - mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_UPLOAD, null)); - } - - /* - * This is useful, but not necessarily needed for the public API. If so desired, someone can uncomment this out. - */ - // /** - // * Initiates an upload of any Localytics data for this session's API key. This should be done early in the process life in - // * order to guarantee as much time as possible for slow connections to complete. It is necessary to do this even if the user - // * has opted out because this is how the opt out is transported to the webservice. - // * - // * @param callback a Runnable to execute when the upload completes. A typical use case would be to notify the caller that - // the - // * upload has completed. This runnable will be executed on an undefined thread, so the caller should anticipate - // * this runnable NOT executing on the main thread or the thread that calls {@link #upload}. This parameter may be - // * null. - // */ - // public void upload(final Runnable callback) - // { - // mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_UPLOAD, callback)); - // } - - /** - * Sorts an int value into a set of regular intervals as defined by the minimum, maximum, and step size. Both the min and max - * values are inclusive, and in the instance where (max - min + 1) is not evenly divisible by step size, the method guarantees - * only the minimum and the step size to be accurate to specification, with the new maximum will be moved to the next regular - * step. - * - * @param actualValue The int value to be sorted. - * @param minValue The int value representing the inclusive minimum interval. - * @param maxValue The int value representing the inclusive maximum interval. - * @param step The int value representing the increment of each interval. - * @return a ranged attribute suitable for passing as the argument to {@link #tagEvent(String)} or - * {@link #tagEvent(String, Map)}. - */ - public static String createRangedAttribute(final int actualValue, final int minValue, final int maxValue, final int step) - { - // Confirm there is at least one bucket - if (step < 1) - { - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, "Step must not be less than zero. Returning null."); //$NON-NLS-1$ - } - return null; - } - if (minValue >= maxValue) - { - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, "maxValue must not be less than minValue. Returning null."); //$NON-NLS-1$ - } - return null; - } - - // Determine the number of steps, rounding up using int math - final int stepQuantity = (maxValue - minValue + step) / step; - final int[] steps = new int[stepQuantity + 1]; - for (int currentStep = 0; currentStep <= stepQuantity; currentStep++) - { - steps[currentStep] = minValue + (currentStep) * step; - } - return createRangedAttribute(actualValue, steps); - } - - /** - * Sorts an int value into a predefined, pre-sorted set of intervals, returning a string representing the new expected value. - * The array must be sorted in ascending order, with the first element representing the inclusive lower bound and the last - * element representing the exclusive upper bound. For instance, the array [0,1,3,10] will provide the following buckets: less - * than 0, 0, 1-2, 3-9, 10 or greater. - * - * @param actualValue The int value to be bucketed. - * @param steps The sorted int array representing the bucketing intervals. - * @return String representation of {@code actualValue} that has been bucketed into the range provided by {@code steps}. - * @throws IllegalArgumentException if {@code steps} is null. - * @throws IllegalArgumentException if {@code steps} has length 0. - */ - @SuppressWarnings("nls") - public static String createRangedAttribute(final int actualValue, final int[] steps) - { - if (null == steps) - { - throw new IllegalArgumentException("steps cannot be null"); //$NON-NLS-1$ - } - - if (steps.length == 0) - { - throw new IllegalArgumentException("steps length must be greater than 0"); //$NON-NLS-1$ - } - - String bucket = null; - - // if less than smallest value - if (actualValue < steps[0]) - { - bucket = "less than " + steps[0]; - } - // if greater than largest value - else if (actualValue >= steps[steps.length - 1]) - { - bucket = steps[steps.length - 1] + " and above"; - } - else - { - // binarySearch returns the index of the value, or (-(insertion point) - 1) if not found - int bucketIndex = Arrays.binarySearch(steps, actualValue); - if (bucketIndex < 0) - { - // if the index wasn't found, then we want the value before the insertion point as the lower end - // the special case where the insertion point is 0 is covered above, so we don't have to worry about it here - bucketIndex = (-bucketIndex) - 2; - } - if (steps[bucketIndex] == (steps[bucketIndex + 1] - 1)) - { - bucket = Integer.toString(steps[bucketIndex]); - } - else - { - bucket = steps[bucketIndex] + "-" + (steps[bucketIndex + 1] - 1); //$NON-NLS-1$ - } - } - return bucket; - } - - /** - * Helper class to handle session-related work on the {@link LocalyticsSession#sSessionHandlerThread}. - */ - /* package */static final class SessionHandler extends Handler - { - /** - * Empty handler message to initialize the callback. - *

- * 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 . - */ - private static final String UPLOAD_BLOBS_EVENTS_SORT_ORDER = String.format("CAST(%s AS TEXT)", UploadBlobEventsDbColumns.EVENTS_KEY_REF); //$NON-NLS-1$ - - /** - * Sort order for the events. - *

- * This is a workaround for Android bug 3707 . - */ - private static final String EVENTS_SORT_ORDER = String.format("CAST(%s as TEXT)", EventsDbColumns._ID); //$NON-NLS-1$ - - /** - * Application context - */ - private final Context mContext; - - /** - * Localytics database - */ - private LocalyticsProvider mProvider; - - /** - * The Localytics API key for the session. - */ - private final String mApiKey; - - /** - * {@link ApiKeysDbColumns#_ID} for the {@link LocalyticsSession#mLocalyticsKey}. - */ - private long mApiKeyId; - - /** - * {@link SessionsDbColumns#_ID} for the session. - */ - private long mSessionId; - - /** - * Flag variable indicating whether {@link #MESSAGE_OPEN} has been received yet. - */ - private boolean mIsSessionOpen = false; - - /** - * Flag variable indicating whether the user has opted out of data collection. - */ - private boolean mIsOptedOut = false; - - /** - * Handler object where all upload of this instance of LocalyticsSession are handed off to. - *

- * 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> pair = (Pair>) msg.obj; - final String event = pair.first; - final Map attributes = pair.second; - - SessionHandler.this.tagEvent(event, attributes); - - break; - } - case MESSAGE_TAG_SCREEN: - { - if (Constants.IS_LOGGABLE) - { - Log.d(Constants.LOG_TAG, "Handler received MESSAGE_SCREEN"); //$NON-NLS-1$ - } - - final String screen = (String) msg.obj; - - SessionHandler.this.tagScreen(screen); - - break; - } - case MESSAGE_UPLOAD: - { - if (Constants.IS_LOGGABLE) - { - Log.d(Constants.LOG_TAG, "SessionHandler received MESSAGE_UPLOAD"); //$NON-NLS-1$ - } - - /* - * Note that callback may be null - */ - final Runnable callback = (Runnable) msg.obj; - - SessionHandler.this.upload(callback); - - break; - } - case MESSAGE_UPLOAD_COMPLETE: - { - if (Constants.IS_LOGGABLE) - { - Log.d(Constants.LOG_TAG, "Handler received MESSAGE_UPLOAD_COMPLETE"); //$NON-NLS-1$ - } - - sIsUploadingMap.put(mApiKey, Boolean.FALSE); - - break; - } - default: - { - /* - * This should never happen - */ - throw new RuntimeException("Fell through switch statement"); //$NON-NLS-1$ - } - } - } - - /** - * Initialize the handler post construction. - *

- * 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 attributes) - { - if (!mIsSessionOpen) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "Tag not written because the session was not open"); //$NON-NLS-1$ - } - return; - } - - /* - * First insert the event - */ - final long eventId; - { - final ContentValues values = new ContentValues(); - values.put(EventsDbColumns.SESSION_KEY_REF, Long.valueOf(mSessionId)); - values.put(EventsDbColumns.UUID, UUID.randomUUID().toString()); - values.put(EventsDbColumns.EVENT_NAME, event); - values.put(EventsDbColumns.REAL_TIME, Long.valueOf(SystemClock.elapsedRealtime())); - values.put(EventsDbColumns.WALL_TIME, Long.valueOf(System.currentTimeMillis())); - - /* - * Special case for open event: keep the start time in sync with the start time put into the sessions table. - */ - if (OPEN_EVENT.equals(event)) - { - Cursor cursor = null; - try - { - cursor = mProvider.query(SessionsDbColumns.TABLE_NAME, new String[] - { SessionsDbColumns.SESSION_START_WALL_TIME }, String.format("%s = ?", SessionsDbColumns._ID), new String[] { Long.toString(mSessionId) }, null); //$NON-NLS-1$ - - if (cursor.moveToFirst()) - { - values.put(EventsDbColumns.WALL_TIME, Long.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(SessionsDbColumns.SESSION_START_WALL_TIME)))); - } - else - { - // this should never happen - throw new RuntimeException("Session didn't exist"); //$NON-NLS-1$ - } - } - finally - { - if (null != cursor) - { - cursor.close(); - } - } - } - - eventId = mProvider.insert(EventsDbColumns.TABLE_NAME, values); - - if (-1 == eventId) - { - throw new RuntimeException("Inserting event failed"); //$NON-NLS-1$ - } - } - - /* - * If attributes exist, insert them as well - */ - if (null != attributes) - { - int count = 0; - for (final Entry entry : attributes.entrySet()) - { - /* - * Note: the attributes that are skipped are deterministic, because the map is actually an instance of - * TreeMap. - */ - count++; - if (count > Constants.MAX_NUM_ATTRIBUTES) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, String.format("Map contains %s keys while the maximum number of attributes is %s. Some attributes were not written. Consider reducing the number of attributes.", Integer.valueOf(attributes.size()), Integer.valueOf(Constants.MAX_NUM_ATTRIBUTES))); //$NON-NLS-1$ - } - break; - } - - final ContentValues values = new ContentValues(); - values.put(AttributesDbColumns.EVENTS_KEY_REF, Long.valueOf(eventId)); - values.put(AttributesDbColumns.ATTRIBUTE_KEY, entry.getKey()); - values.put(AttributesDbColumns.ATTRIBUTE_VALUE, entry.getValue()); - - final long id = mProvider.insert(AttributesDbColumns.TABLE_NAME, values); - - if (-1 == id) - { - throw new RuntimeException("Inserting attribute failed"); //$NON-NLS-1$ - } - } - } - - /* - * Insert the event into the history, only for application events - */ - if (!OPEN_EVENT.equals(event) && !CLOSE_EVENT.equals(event) && !OPT_IN_EVENT.equals(event) && !OPT_OUT_EVENT.equals(event) && !FLOW_EVENT.equals(event)) - { - final ContentValues values = new ContentValues(); - values.put(EventHistoryDbColumns.NAME, event.substring(mContext.getPackageName().length() + 1, event.length())); - values.put(EventHistoryDbColumns.TYPE, Integer.valueOf(EventHistoryDbColumns.TYPE_EVENT)); - values.put(EventHistoryDbColumns.SESSION_KEY_REF, Long.valueOf(mSessionId)); - values.putNull(EventHistoryDbColumns.PROCESSED_IN_BLOB); - mProvider.insert(EventHistoryDbColumns.TABLE_NAME, values); - - conditionallyAddFlowEvent(); - } - } - - /** - * Tag a screen 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 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 eventIds = new HashSet(); - - Cursor eventsCursor = null; - Cursor blob_eventsCursor = null; - try - { - eventsCursor = mProvider.query(EventsDbColumns.TABLE_NAME, new String[] - { - EventsDbColumns._ID, - EventsDbColumns.EVENT_NAME, - EventsDbColumns.WALL_TIME }, null, null, EVENTS_SORT_ORDER); - - 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 (CLOSE_EVENT.equals(eventsCursor.getString(eventsCursor.getColumnIndexOrThrow(EventsDbColumns.EVENT_NAME)))) - { - if (System.currentTimeMillis() - eventsCursor.getLong(eventsCursor.getColumnIndexOrThrow(EventsDbColumns.WALL_TIME)) < Constants.SESSION_EXPIRATION) - { - break; - } - } - eventIds.add(Long.valueOf(eventsCursor.getLong(idColumn))); - break; - } - case BOTH: - break; - case RIGHT: - break; - } - } - } - finally - { - if (eventsCursor != null) - { - eventsCursor.close(); - } - - if (blob_eventsCursor != null) - { - blob_eventsCursor.close(); - } - } - - if (eventIds.size() > 0) - { - final long blobId; - { - final ContentValues values = new ContentValues(); - values.put(UploadBlobsDbColumns.UUID, UUID.randomUUID().toString()); - blobId = mProvider.insert(UploadBlobsDbColumns.TABLE_NAME, values); - } - - { - final ContentValues values = new ContentValues(); - for (final Long x : eventIds) - { - values.clear(); - - values.put(UploadBlobEventsDbColumns.UPLOAD_BLOBS_KEY_REF, Long.valueOf(blobId)); - values.put(UploadBlobEventsDbColumns.EVENTS_KEY_REF, x); - - mProvider.insert(UploadBlobEventsDbColumns.TABLE_NAME, values); - } - } - - final ContentValues values = new ContentValues(); - values.put(EventHistoryDbColumns.PROCESSED_IN_BLOB, Long.valueOf(blobId)); - mProvider.update(EventHistoryDbColumns.TABLE_NAME, values, String.format("%s IS NULL", EventHistoryDbColumns.PROCESSED_IN_BLOB), null); //$NON-NLS-1$ - } - } - - /** - * Initiate upload of all session data currently stored on disk. - *

- * 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 toUpload = convertDatabaseToJson(); - - if (!toUpload.isEmpty()) - { - final StringBuilder builder = new StringBuilder(); - for (final JSONObject json : toUpload) - { - builder.append(json.toString()); - builder.append('\n'); - } - - if (uploadSessions(String.format(ANALYTICS_URL, mApiKey), builder.toString())) - { - mProvider.runBatchTransaction(new Runnable() - { - @Override - public void run() - { - deleteBlobsAndSessions(mProvider); - } - }); - } - } - } - finally - { - if (callback != null) - { - /* - * Execute the callback on a separate thread, to avoid exposing this thread to the client of the - * library - */ - new Thread(callback, UPLOAD_CALLBACK_THREAD_NAME).start(); - } - - mSessionHandler.sendEmptyMessage(SessionHandler.MESSAGE_UPLOAD_COMPLETE); - } - break; - } - case MESSAGE_RETRY_UPLOAD_REQUEST: - { - if (Constants.IS_LOGGABLE) - { - Log.d(Constants.LOG_TAG, "Received MESSAGE_RETRY_UPLOAD_REQUEST"); //$NON-NLS-1$ - } - - mSessionHandler.sendMessage(mSessionHandler.obtainMessage(SessionHandler.MESSAGE_UPLOAD, msg.obj)); - break; - } - default: - { - /* - * This should never happen - */ - throw new RuntimeException("Fell through switch statement"); //$NON-NLS-1$ - } - } - } - - /** - * Uploads the post Body to the webservice - * - * @param url where {@code body} will be posted to. Cannot be null. - * @param body upload body as a string. This should be a plain old string. Cannot be null. - * @return True on success, false on failure. - */ - /* package */static boolean uploadSessions(final String url, final String body) - { - if (Constants.ENABLE_PARAMETER_CHECKING) - { - if (null == url) - { - throw new IllegalArgumentException("url cannot be null"); //$NON-NLS-1$ - } - - if (null == body) - { - throw new IllegalArgumentException("body cannot be null"); //$NON-NLS-1$ - } - } - - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("Upload body before compression is: %s", body.toString())); //$NON-NLS-1$ - } - - final DefaultHttpClient client = new DefaultHttpClient(); - final HttpPost method = new HttpPost(url); - method.addHeader("Content-Type", "application/x-gzip"); //$NON-NLS-1$ //$NON-NLS-2$ - - GZIPOutputStream gos = null; - try - { - final byte[] originalBytes = body.getBytes("UTF-8"); //$NON-NLS-1$ - final ByteArrayOutputStream baos = new ByteArrayOutputStream(originalBytes.length); - gos = new GZIPOutputStream(baos); - gos.write(originalBytes); - gos.finish(); - gos.flush(); - - final ByteArrayEntity postBody = new ByteArrayEntity(baos.toByteArray()); - method.setEntity(postBody); - - final HttpResponse response = client.execute(method); - - final StatusLine status = response.getStatusLine(); - final int statusCode = status.getStatusCode(); - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("Upload complete with status %d", Integer.valueOf(statusCode))); //$NON-NLS-1$ - } - - /* - * 5xx status codes indicate a server error, so upload should be reattempted - */ - if (statusCode >= 500 && statusCode <= 599) - { - return false; - } - - return true; - } - catch (final UnsupportedEncodingException e) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "UnsupportedEncodingException", e); //$NON-NLS-1$ - } - return false; - } - catch (final ClientProtocolException e) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "ClientProtocolException", e); //$NON-NLS-1$ - } - return false; - } - catch (final IOException e) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "IOException", e); //$NON-NLS-1$ - } - return false; - } - finally - { - if (null != gos) - { - try - { - gos.close(); - } - catch (final IOException e) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$ - } - } - } - } - } - - /** - * Helper that converts blobs in the database into a JSON representation for upload. - * - * @return A list of JSON objecs to upload to the server - */ - /* package */List convertDatabaseToJson() - { - final List result = new LinkedList(); - Cursor cursor = null; - try - { - cursor = mProvider.query(UploadBlobsDbColumns.TABLE_NAME, null, null, null, null); - - final long creationTime = getApiKeyCreationTime(mProvider, mApiKey); - - final int idColumn = cursor.getColumnIndexOrThrow(UploadBlobsDbColumns._ID); - final int uuidColumn = cursor.getColumnIndexOrThrow(UploadBlobsDbColumns.UUID); - while (cursor.moveToNext()) - { - try - { - final JSONObject blobHeader = new JSONObject(); - - blobHeader.put(JsonObjects.BlobHeader.KEY_DATA_TYPE, BlobHeader.VALUE_DATA_TYPE); - blobHeader.put(JsonObjects.BlobHeader.KEY_PERSISTENT_STORAGE_CREATION_TIME_SECONDS, creationTime); - blobHeader.put(JsonObjects.BlobHeader.KEY_SEQUENCE_NUMBER, cursor.getLong(idColumn)); - blobHeader.put(JsonObjects.BlobHeader.KEY_UNIQUE_ID, cursor.getString(uuidColumn)); - blobHeader.put(JsonObjects.BlobHeader.KEY_ATTRIBUTES, getAttributesFromSession(mProvider, mApiKey, getSessionIdForBlobId(cursor.getLong(idColumn)))); - result.add(blobHeader); - - Cursor blobEvents = null; - try - { - blobEvents = 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(cursor.getLong(idColumn)) }, UploadBlobEventsDbColumns.EVENTS_KEY_REF); - - final int eventIdColumn = blobEvents.getColumnIndexOrThrow(UploadBlobEventsDbColumns.EVENTS_KEY_REF); - while (blobEvents.moveToNext()) - { - result.add(convertEventToJson(mProvider, mContext, blobEvents.getLong(eventIdColumn), cursor.getLong(idColumn), mApiKey)); - } - } - finally - { - if (null != blobEvents) - { - blobEvents.close(); - } - } - } - catch (final JSONException e) - { - if (Constants.IS_LOGGABLE) - { - Log.w(Constants.LOG_TAG, "Caught exception", e); //$NON-NLS-1$ - } - } - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } - - if (Constants.IS_LOGGABLE) - { - Log.v(Constants.LOG_TAG, String.format("JSON result is %s", result.toString())); //$NON-NLS-1$ - } - - return result; - } - - /** - * Deletes all blobs and sessions/events/attributes associated with those blobs. - *

- * 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 sessionsToDelete = new LinkedList(); - final HashSet blobsToDelete = new HashSet(); - - Cursor blobEvents = null; - try - { - blobEvents = provider.query(UploadBlobEventsDbColumns.TABLE_NAME, new String[] - { - UploadBlobEventsDbColumns._ID, - UploadBlobEventsDbColumns.EVENTS_KEY_REF, - UploadBlobEventsDbColumns.UPLOAD_BLOBS_KEY_REF }, null, null, null); - - final int uploadBlobIdColumn = blobEvents.getColumnIndexOrThrow(UploadBlobEventsDbColumns.UPLOAD_BLOBS_KEY_REF); - final int blobEventIdColumn = blobEvents.getColumnIndexOrThrow(UploadBlobEventsDbColumns._ID); - final int eventIdColumn = blobEvents.getColumnIndexOrThrow(UploadBlobEventsDbColumns.EVENTS_KEY_REF); - while (blobEvents.moveToNext()) - { - final long blobId = blobEvents.getLong(uploadBlobIdColumn); - final long blobEventId = blobEvents.getLong(blobEventIdColumn); - final long eventId = blobEvents.getLong(eventIdColumn); - - // delete the blobevent - provider.delete(UploadBlobEventsDbColumns.TABLE_NAME, String.format("%s = ?", UploadBlobEventsDbColumns._ID), new String[] { Long.toString(blobEventId) }); //$NON-NLS-1$ - - /* - * Add the blob to the list of blobs to be deleted - */ - blobsToDelete.add(Long.valueOf(blobId)); - - // delete all attributes for the event - provider.delete(AttributesDbColumns.TABLE_NAME, String.format("%s = ?", AttributesDbColumns.EVENTS_KEY_REF), new String[] { Long.toString(eventId) }); //$NON-NLS-1$ - - /* - * Check to see if the event is a close event, indicating that the session is complete and can also be deleted - */ - Cursor eventCursor = null; - try - { - eventCursor = provider.query(EventsDbColumns.TABLE_NAME, new String[] - { EventsDbColumns.SESSION_KEY_REF }, String.format("%s = ? AND %s = ?", EventsDbColumns._ID, EventsDbColumns.EVENT_NAME), new String[] //$NON-NLS-1$ - { - Long.toString(eventId), - CLOSE_EVENT }, null); - - if (eventCursor.moveToFirst()) - { - final long sessionId = eventCursor.getLong(eventCursor.getColumnIndexOrThrow(EventsDbColumns.SESSION_KEY_REF)); - - provider.delete(EventHistoryDbColumns.TABLE_NAME, String.format("%s = ?", EventHistoryDbColumns.SESSION_KEY_REF), new String[] //$NON-NLS-1$ - { Long.toString(sessionId) }); - - sessionsToDelete.add(Long.valueOf(eventCursor.getLong(eventCursor.getColumnIndexOrThrow(EventsDbColumns.SESSION_KEY_REF)))); - } - } - finally - { - if (null != eventCursor) - { - eventCursor.close(); - } - } - - // delete the event - provider.delete(EventsDbColumns.TABLE_NAME, String.format("%s = ?", EventsDbColumns._ID), new String[] { Long.toString(eventId) }); //$NON-NLS-1$ - } - } - finally - { - if (null != blobEvents) - { - blobEvents.close(); - } - } - - // delete blobs - for (final long x : blobsToDelete) - { - provider.delete(UploadBlobsDbColumns.TABLE_NAME, String.format("%s = ?", UploadBlobsDbColumns._ID), new String[] { Long.toString(x) }); //$NON-NLS-1$ - } - - // delete sessions - for (final long x : sessionsToDelete) - { - provider.delete(SessionsDbColumns.TABLE_NAME, String.format("%s = ?", SessionsDbColumns._ID), new String[] { Long.toString(x) }); //$NON-NLS-1$ - } - - } - - /** - * Gets the creation time for an API key. - * - * @param provider Localytics database provider. Cannot be null. - * @param key Localytics API key. Cannot be null. - * @return The time in seconds since the Unix Epoch when the API key entry was created in the database. - * @throws RuntimeException if the API key entry doesn't exist in the database. - */ - /* package */static long getApiKeyCreationTime(final LocalyticsProvider provider, final String key) - { - Cursor cursor = null; - try - { - cursor = provider.query(ApiKeysDbColumns.TABLE_NAME, null, String.format("%s = ?", ApiKeysDbColumns.API_KEY), new String[] { key }, null); //$NON-NLS-1$ - - if (cursor.moveToFirst()) - { - return Math.round((float) cursor.getLong(cursor.getColumnIndexOrThrow(ApiKeysDbColumns.CREATED_TIME)) / DateUtils.SECOND_IN_MILLIS); - } - - /* - * This should never happen - */ - throw new RuntimeException("API key entry couldn't be found"); //$NON-NLS-1$ - } - finally - { - if (null != cursor) - { - cursor.close(); - } - } - } - - /** - * Helper method to generate the attributes object for a session - * - * @param provider Instance of the Localytics database provider. Cannot be null. - * @param apiKey Localytics API key. Cannot be null. - * @param sessionId The {@link SessionsDbColumns#_ID} of the session. - * @return a JSONObject representation of the session attributes - * @throws JSONException if a problem occurred converting the element to JSON. - */ - /* package */static JSONObject getAttributesFromSession(final LocalyticsProvider provider, final String apiKey, final long sessionId) throws JSONException - { - Cursor cursor = null; - try - { - cursor = provider.query(SessionsDbColumns.TABLE_NAME, null, String.format("%s = ?", SessionsDbColumns._ID), new String[] { Long.toString(sessionId) }, null); //$NON-NLS-1$ - - if (cursor.moveToFirst()) - { - final JSONObject result = new JSONObject(); - result.put(JsonObjects.BlobHeader.Attributes.KEY_CLIENT_APP_VERSION, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.APP_VERSION))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DATA_CONNECTION, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.NETWORK_TYPE))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_ANDROID_ID_HASH, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_ANDROID_ID_HASH))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_COUNTRY, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_COUNTRY))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_MANUFACTURER, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_MANUFACTURER))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_MODEL, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_MODEL))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_OS_VERSION, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.ANDROID_VERSION))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_PLATFORM, JsonObjects.BlobHeader.Attributes.VALUE_PLATFORM); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_SERIAL_HASH, cursor.isNull(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_SERIAL_NUMBER_HASH)) ? JSONObject.NULL - : cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_SERIAL_NUMBER_HASH))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_SDK_LEVEL, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.ANDROID_SDK))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_DEVICE_TELEPHONY_ID, cursor.isNull(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_TELEPHONY_ID)) ? JSONObject.NULL - : cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.DEVICE_TELEPHONY_ID))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_LOCALYTICS_API_KEY, apiKey); - result.put(JsonObjects.BlobHeader.Attributes.KEY_LOCALYTICS_CLIENT_LIBRARY_VERSION, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.LOCALYTICS_LIBRARY_VERSION))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_LOCALYTICS_DATA_TYPE, JsonObjects.BlobHeader.Attributes.VALUE_DATA_TYPE); - result.put(JsonObjects.BlobHeader.Attributes.KEY_LOCALE_COUNTRY, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.LOCALE_COUNTRY))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_LOCALE_LANGUAGE, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.LOCALE_LANGUAGE))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_NETWORK_CARRIER, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.NETWORK_CARRIER))); - result.put(JsonObjects.BlobHeader.Attributes.KEY_NETWORK_COUNTRY, cursor.getString(cursor.getColumnIndexOrThrow(SessionsDbColumns.NETWORK_COUNTRY))); - - return result; - } - - throw new RuntimeException("No session exists"); //$NON-NLS-1$ - } - finally - { - if (null != cursor) - { - cursor.close(); - } - } - } - - /** - * Converts an event into a JSON object. - *

- * 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 - { - public final F first; - - public final S second; - - public Pair(final F first, final S second) - { - this.first = first; - this.second = second; - } - } -} diff --git a/astrid/common-src/com/localytics/android/ReflectionUtils.java b/astrid/common-src/com/localytics/android/ReflectionUtils.java deleted file mode 100644 index 202c700ac..000000000 --- a/astrid/common-src/com/localytics/android/ReflectionUtils.java +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2012 Todoroo Inc - * - * See the file "LICENSE" for the full license governing this code. - */ -package com.localytics.android; - -import java.lang.reflect.InvocationTargetException; - -/** - * Static utilities for performing reflection against newer Android SDKs. - *

- * 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 Type that the method should return - * @param classObject Class on which to invoke {@code methodName}. Cannot be null. - * @param methodName Name of the method to invoke. Cannot be null. - * @param types explicit types for the objects. This is useful if the types are primitives, rather than objects. - * @param args arguments for the method. May be null if the method takes no arguments. - * @return The result of invoking the named method on the given class for the args - * @throws RuntimeException if the class or method doesn't exist - */ - public static T tryInvokeStatic(final Class classObject, final String methodName, final Class[] types, final Object[] args) - { - return (T) helper(null, classObject, null, methodName, types, args); - } - - /** - * Use reflection to invoke a static method for a class object and method name - * - * @param Type that the method should return - * @param className Name of the class on which to invoke {@code methodName}. Cannot be null. - * @param methodName Name of the method to invoke. Cannot be null. - * @param types explicit types for the objects. This is useful if the types are primitives, rather than objects. - * @param args arguments for the method. May be null if the method takes no arguments. - * @return The result of invoking the named method on the given class for the args - * @throws RuntimeException if the class or method doesn't exist - */ - public static T tryInvokeStatic(final String className, final String methodName, final Class[] types, final Object[] args) - { - return (T) helper(className, null, null, methodName, types, args); - } - - /** - * Use reflection to invoke a static method for a class object and method name - * - * @param Type that the method should return - * @param target Object instance on which to invoke {@code methodName}. Cannot be null. - * @param methodName Name of the method to invoke. Cannot be null. - * @param types explicit types for the objects. This is useful if the types are primitives, rather than objects. - * @param args arguments for the method. May be null if the method takes no arguments. - * @return The result of invoking the named method on the given class for the args - * @throws RuntimeException if the class or method doesn't exist - */ - public static T tryInvokeInstance(final Object target, final String methodName, final Class[] types, final Object[] args) - { - return (T) helper(target, null, null, methodName, types, args); - } - - @SuppressWarnings("unchecked") - private static T helper(final Object target, final Class classObject, final String className, final String methodName, final Class[] argTypes, final Object[] args) - { - try - { - Class cls; - if (classObject != null) - { - cls = classObject; - } - else if (target != null) - { - cls = target.getClass(); - } - else - { - cls = Class.forName(className); - } - - return (T) cls.getMethod(methodName, argTypes).invoke(target, args); - } - catch (final NoSuchMethodException e) - { - throw new RuntimeException(e); - } - catch (final IllegalAccessException e) - { - throw new RuntimeException(e); - } - catch (final InvocationTargetException e) - { - throw new RuntimeException(e); - } - catch (final ClassNotFoundException e) - { - throw new RuntimeException(e); - } - } -} diff --git a/astrid/libs/crittercism_v3_0_7_sdkonly.jar b/astrid/libs/crittercism_v3_0_7_sdkonly.jar deleted file mode 100644 index e5d4c0831725e34cf85c85923aa6161d34b4118a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58125 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∓sAk`LY1URlF$;JOJ3&AqQ&aLso8xH?!K) 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 diff --git a/astrid/libs/findbugs-annotations.jar b/astrid/libs/findbugs-annotations.jar deleted file mode 100644 index ce70abef4894efa4c1102e61cbe519538845f3e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14551 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 Czds59 diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmBackgroundService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmBackgroundService.java index 71b36794a..e64efa416 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmBackgroundService.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmBackgroundService.java @@ -9,7 +9,6 @@ import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; import com.todoroo.astrid.actfm.sync.ActFmSyncV2Provider; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.sync.SyncProviderUtilities; import com.todoroo.astrid.sync.SyncV2BackgroundService; import com.todoroo.astrid.sync.SyncV2Provider; @@ -43,12 +42,10 @@ public class ActFmBackgroundService extends SyncV2BackgroundService { @Override public void onCreate() { super.onCreate(); - StatisticsService.sessionStart(this); } @Override public void onDestroy() { - StatisticsService.sessionStop(this); super.onDestroy(); } diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmGoogleAuthActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmGoogleAuthActivity.java index b3db25c82..d882d953f 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmGoogleAuthActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmGoogleAuthActivity.java @@ -30,7 +30,6 @@ import com.google.api.client.googleapis.extensions.android2.auth.GoogleAccountMa import com.timsu.astrid.R; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.utility.DialogUtilities; -import com.todoroo.astrid.service.StatisticsService; /** * This activity allows users to sign in or log in to Google Tasks @@ -163,19 +162,16 @@ public class ActFmGoogleAuthActivity extends ListActivity { @Override protected void onResume() { super.onResume(); - StatisticsService.sessionStart(this); } @Override protected void onPause() { super.onPause(); - StatisticsService.sessionPause(); } @Override protected void onStop() { super.onStop(); - StatisticsService.sessionStop(this); } private static final int REQUEST_AUTHENTICATE = 0; diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java index f8a2cb0ed..bf3d3bc0b 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java @@ -106,7 +106,6 @@ import com.todoroo.astrid.helper.UUIDHelper; import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.MarketStrategy.AmazonMarketStrategy; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.SyncV2Service; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.subtasks.AstridOrderedListUpdater; @@ -232,14 +231,12 @@ public class ActFmLoginActivity extends SherlockFragmentActivity { protected void onResume() { super.onResume(); uiHelper.onResume(); - StatisticsService.sessionStart(this); } @Override protected void onPause() { super.onPause(); uiHelper.onPause(); - StatisticsService.sessionPause(); } @Override @@ -250,12 +247,10 @@ public class ActFmLoginActivity extends SherlockFragmentActivity { @Override protected void onStop() { - StatisticsService.sessionStop(this); super.onStop(); } protected void recordPageView() { - StatisticsService.reportEvent(StatisticsConstants.ACTFM_LOGIN_SHOW); } protected void setupTermsOfService(TextView tos) { @@ -331,7 +326,6 @@ public class ActFmLoginActivity extends SherlockFragmentActivity { Intent intent = new Intent(ActFmLoginActivity.this, ActFmGoogleAuthActivity.class); startActivityForResult(intent, REQUEST_CODE_GOOGLE); - StatisticsService.reportEvent(StatisticsConstants.ACTFM_LOGIN_GL_START); } }; @@ -395,7 +389,6 @@ public class ActFmLoginActivity extends SherlockFragmentActivity { AndroidUtilities.hideSoftInputForViews(ActFmLoginActivity.this, firstNameField, lastNameField, email); authenticate(email.getText().toString(), firstName, lastName, ActFmInvoker.PROVIDER_PASSWORD, generateRandomPassword()); - StatisticsService.reportEvent(StatisticsConstants.ACTFM_SIGNUP_PW); } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override @@ -462,7 +455,6 @@ public class ActFmLoginActivity extends SherlockFragmentActivity { authenticate(email.getText().toString(), "", "", ActFmInvoker.PROVIDER_PASSWORD, //$NON-NLS-1$//$NON-NLS-2$ password.getText().toString()); - StatisticsService.reportEvent(StatisticsConstants.ACTFM_LOGIN_PW); } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override @@ -594,7 +586,6 @@ public class ActFmLoginActivity extends SherlockFragmentActivity { final String token = actFmInvoker.getToken(); if (result.optBoolean("new")) { // Report new user statistic - StatisticsService.reportEvent(StatisticsConstants.ACTFM_NEW_USER, "provider", provider); } // Successful login, create outstanding entries String lastId = ActFmPreferenceService.userId(); //Preferences.getLong(ActFmPreferenceService.PREF_USER_ID, 0); diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmPreferences.java b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmPreferences.java index b232379d7..5adebd9b3 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/ActFmPreferences.java @@ -29,7 +29,6 @@ import com.todoroo.astrid.billing.BillingActivity; import com.todoroo.astrid.gtasks.GtasksPreferenceService; import com.todoroo.astrid.service.PremiumUnlockService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.sync.SyncProviderPreferences; import com.todoroo.astrid.sync.SyncProviderUtilities; import com.todoroo.astrid.utility.Constants; @@ -208,7 +207,6 @@ public class ActFmPreferences extends SyncProviderPreferences { } else { Intent intent = new Intent(this, BillingActivity.class); startActivity(intent); - StatisticsService.reportEvent(StatisticsConstants.PREMIUM_PAGE_VIEWED); } } diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/CommentsFragment.java b/astrid/plugin-src/com/todoroo/astrid/actfm/CommentsFragment.java index a54a6cec5..fe1984a20 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/CommentsFragment.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/CommentsFragment.java @@ -56,7 +56,6 @@ import com.todoroo.astrid.dao.UserActivityDao; import com.todoroo.astrid.data.RemoteModel; import com.todoroo.astrid.data.UserActivity; import com.todoroo.astrid.helper.AsyncImageView; -import com.todoroo.astrid.service.StatisticsService; import edu.mit.mobile.android.imagecache.ImageCache; @@ -408,8 +407,6 @@ public abstract class CommentsFragment extends SherlockListFragment { resetPictureButton(); refreshUpdatesList(); - - StatisticsService.reportEvent(commentAddStatistic()); } @Override diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java index 22bf21220..35ce01d97 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/EditPeopleControlSet.java @@ -62,7 +62,6 @@ import com.todoroo.astrid.helper.AsyncImageView; import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TagDataService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.ThemeService; @@ -714,9 +713,7 @@ public class EditPeopleControlSet extends PopupControlSet { task.putTransitory(TaskService.TRANS_ASSIGNED, true); if (assignedView == assignedCustom) { - StatisticsService.reportEvent(StatisticsConstants.TASK_ASSIGNED_EMAIL); } else if (task.getValue(Task.USER_ID) != Task.USER_ID_SELF) { - StatisticsService.reportEvent(StatisticsConstants.TASK_ASSIGNED_PICKER); } return true; diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/TagSettingsActivity.java b/astrid/plugin-src/com/todoroo/astrid/actfm/TagSettingsActivity.java index 6bb882f94..818170a55 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/TagSettingsActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/TagSettingsActivity.java @@ -60,7 +60,6 @@ import com.todoroo.astrid.data.User; import com.todoroo.astrid.helper.AsyncImageView; import com.todoroo.astrid.helper.UUIDHelper; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TagDataService; import com.todoroo.astrid.service.ThemeService; import com.todoroo.astrid.tags.TagFilterExposer; @@ -376,7 +375,6 @@ public class TagSettingsActivity extends SherlockFragmentActivity { int oldMemberCount = tagData.getValue(TagData.MEMBER_COUNT); if (members.length() > oldMemberCount) { - StatisticsService.reportEvent(StatisticsConstants.ACTFM_LIST_SHARED); } tagData.setValue(TagData.MEMBER_COUNT, members.length()); tagData.setFlag(TagData.FLAGS, TagData.FLAG_SILENT, isSilent.isChecked()); diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmPreferenceService.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmPreferenceService.java index 6e0537702..aac2936f7 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmPreferenceService.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmPreferenceService.java @@ -17,7 +17,6 @@ import com.todoroo.astrid.dao.RemoteModelDao; import com.todoroo.astrid.data.RemoteModel; import com.todoroo.astrid.service.PremiumUnlockService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.sync.SyncProviderUtilities; import com.todoroo.astrid.utility.AstridPreferences; @@ -121,7 +120,6 @@ public class ActFmPreferenceService extends SyncProviderUtilities { @Override protected void reportLastErrorImpl(String lastError, String type) { - StatisticsService.reportEvent(StatisticsConstants.ACTFM_SYNC_ERROR, "type", type); //$NON-NLS-1$ } public synchronized static JSONObject thisUser() { diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncThread.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncThread.java index 3e09af773..e28ef425a 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncThread.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/ActFmSyncThread.java @@ -25,7 +25,6 @@ import android.net.NetworkInfo; import android.support.v4.app.NotificationCompat; import android.util.Log; -import com.crittercism.app.Crittercism; import com.timsu.astrid.R; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; @@ -381,7 +380,6 @@ public class ActFmSyncThread { } catch (Exception e) { // In the worst case, restart thread if something goes wrong Log.e(ERROR_TAG, "Unexpected sync thread exception", e); - Crittercism.logHandledException(e); thread = null; startSyncThread(); } diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/AstridNewSyncMigrator.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/AstridNewSyncMigrator.java index c5e980bbe..7c21ce9b5 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/AstridNewSyncMigrator.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/AstridNewSyncMigrator.java @@ -6,7 +6,6 @@ import java.util.Set; import android.text.TextUtils; import android.util.Log; -import com.crittercism.app.Crittercism; import com.todoroo.andlib.data.DatabaseDao; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.TodorooCursor; @@ -109,12 +108,10 @@ public class AstridNewSyncMigrator { tagDataService.save(newTagData); } catch (Exception e) { Log.e(LOG_TAG, "Error creating tag data", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error creating tag data", e); - Crittercism.logHandledException(e); } finally { if (noTagData != null) { noTagData.close(); @@ -139,11 +136,9 @@ public class AstridNewSyncMigrator { } } catch (Exception e) { Log.e(LOG_TAG, "Error clearing emergent tags"); - Crittercism.logHandledException(e); } } } catch (Exception e){ - Crittercism.logHandledException(e); } finally { if (emergentTags != null) { emergentTags.close(); @@ -192,7 +187,6 @@ public class AstridNewSyncMigrator { }); } catch (Exception e) { Log.e(LOG_TAG, "Error asserting UUIDs", e); - Crittercism.logHandledException(e); } // -------------- @@ -207,7 +201,6 @@ public class AstridNewSyncMigrator { taskDao.update(Functions.bitwiseAnd(Task.FLAGS, Task.FLAG_PUBLIC).gt(0), template); } catch (Exception e) { Log.e(LOG_TAG, "Error clearing task flags", e); - Crittercism.logHandledException(e); } // -------------- @@ -239,12 +232,10 @@ public class AstridNewSyncMigrator { } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating recurrence", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating recurrence", e); - Crittercism.logHandledException(e); } finally { if (tasksWithRecurrence != null) { tasksWithRecurrence.close(); @@ -291,13 +282,11 @@ public class AstridNewSyncMigrator { } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating updates", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating updates", e); - Crittercism.logHandledException(e); } finally { if (updates != null) { updates.close(); @@ -313,7 +302,6 @@ public class AstridNewSyncMigrator { userDao.deleteWhere(Criterion.or(User.UUID.isNull(), User.UUID.eq(""), User.UUID.eq("0"))); } catch (Exception e) { Log.e(LOG_TAG, "Error deleting incomplete user entries", e); - Crittercism.logHandledException(e); } // -------------- @@ -369,13 +357,11 @@ public class AstridNewSyncMigrator { taskAttachmentDao.createNew(attachment); } catch (Exception e) { Log.e(LOG_TAG, "Error migrating task attachment metadata", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating task attachment metadata", e); - Crittercism.logHandledException(e); } finally { if (fmCursor != null) { fmCursor.close(); @@ -401,7 +387,6 @@ public class AstridNewSyncMigrator { } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating active tasks ordering", e); - Crittercism.logHandledException(e); } try { @@ -420,7 +405,6 @@ public class AstridNewSyncMigrator { } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating today ordering", e); - Crittercism.logHandledException(e); } TodorooCursor allTagData = null; @@ -441,12 +425,10 @@ public class AstridNewSyncMigrator { taskListMetadataDao.createNew(tlm); } catch (Exception e) { Log.e(LOG_TAG, "Error migrating tag ordering", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error migrating tag ordering", e); - Crittercism.logHandledException(e); } finally { if (allTagData != null) { allTagData.close(); @@ -493,13 +475,11 @@ public class AstridNewSyncMigrator { } catch (Exception e) { Log.e(LOG_TAG, "Error validating task to tag metadata", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error validating task to tag metadata", e); - Crittercism.logHandledException(e); } finally { if (incompleteMetadata != null) { incompleteMetadata.close(); @@ -513,7 +493,6 @@ public class AstridNewSyncMigrator { tagDataDao.deleteWhere(Functions.bitwiseAnd(TagData.FLAGS, TagData.FLAG_FEATURED).gt(0)); } catch (Exception e) { Log.e(LOG_TAG, "Error deleting featured list data", e); - Crittercism.logHandledException(e); } @@ -549,12 +528,10 @@ public class AstridNewSyncMigrator { taskOutstandingDao.createNew(to); } catch (Exception e) { Log.e(LOG_TAG, "Error creating tag_added outstanding entries", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error creating tag_added outstanding entries", e); - Crittercism.logHandledException(e); } finally { if (tagsAdded != null) { tagsAdded.close(); @@ -602,12 +579,10 @@ public class AstridNewSyncMigrator { } } catch (Exception e) { Log.e(LOG_TAG, "Error asserting UUIDs", e); - Crittercism.logHandledException(e); } } } catch (Exception e) { Log.e(LOG_TAG, "Error asserting UUIDs", e); - Crittercism.logHandledException(e); } finally { if (cursor != null) { cursor.close(); diff --git a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/messages/ChangesHappened.java b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/messages/ChangesHappened.java index 08129e69a..da4357f62 100644 --- a/astrid/plugin-src/com/todoroo/astrid/actfm/sync/messages/ChangesHappened.java +++ b/astrid/plugin-src/com/todoroo/astrid/actfm/sync/messages/ChangesHappened.java @@ -14,7 +14,6 @@ import org.json.JSONObject; import android.text.TextUtils; import android.util.Log; -import com.crittercism.app.Crittercism; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property.PropertyVisitor; import com.todoroo.andlib.data.TodorooCursor; @@ -186,7 +185,6 @@ public class ChangesHappened { return null; } } catch (JSONException e) { - Crittercism.logHandledException(e); return null; } } diff --git a/astrid/plugin-src/com/todoroo/astrid/backup/BackupConstants.java b/astrid/plugin-src/com/todoroo/astrid/backup/BackupConstants.java index bb2e517fd..3ef4ff2e8 100644 --- a/astrid/plugin-src/com/todoroo/astrid/backup/BackupConstants.java +++ b/astrid/plugin-src/com/todoroo/astrid/backup/BackupConstants.java @@ -8,7 +8,6 @@ package com.todoroo.astrid.backup; import java.io.File; import android.os.Environment; -import edu.umd.cs.findbugs.annotations.CheckForNull; /** @@ -68,7 +67,6 @@ public class BackupConstants { /** * @return export directory for tasks, or null if no SD card */ - @CheckForNull public static File defaultExportDirectory() { String storageState = Environment.getExternalStorageState(); if (storageState.equals(Environment.MEDIA_MOUNTED)) { diff --git a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java index 64373c67b..776a3cd99 100644 --- a/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/core/CustomFilterActivity.java @@ -56,7 +56,6 @@ import com.todoroo.astrid.api.TextInputCriterion; import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.data.Task; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.ThemeService; import com.todoroo.astrid.utility.AstridPreferences; @@ -277,21 +276,18 @@ public class CustomFilterActivity extends SherlockFragmentActivity { @Override protected void onStop() { - StatisticsService.sessionStop(this); super.onStop(); } @Override protected void onResume() { super.onResume(); - StatisticsService.sessionStart(this); registerReceiver(filterCriteriaReceiver, new IntentFilter(AstridApiConstants.BROADCAST_SEND_CUSTOM_FILTER_CRITERIA)); populateCriteria(); } @Override protected void onPause() { - StatisticsService.sessionPause(); super.onPause(); unregisterReceiver(filterCriteriaReceiver); } diff --git a/astrid/plugin-src/com/todoroo/astrid/gcal/GCalControlSet.java b/astrid/plugin-src/com/todoroo/astrid/gcal/GCalControlSet.java index 61571724c..6dcaf03d2 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gcal/GCalControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/gcal/GCalControlSet.java @@ -35,7 +35,6 @@ import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.gcal.Calendars.CalendarResult; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.ThemeService; import com.todoroo.astrid.ui.PopupControlSet; @@ -149,7 +148,6 @@ public class GCalControlSet extends PopupControlSet { !Preferences.getStringValue(R.string.gcal_p_default).equals("-1"); if ((gcalCreateEventEnabled || calendarSelector.getSelectedItemPosition() != 0) && calendarUri == null) { - StatisticsService.reportEvent(StatisticsConstants.CREATE_CALENDAR_EVENT); try{ ContentResolver cr = activity.getContentResolver(); diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksBackgroundService.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksBackgroundService.java index 3b34324b8..3aed193cf 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksBackgroundService.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksBackgroundService.java @@ -8,7 +8,6 @@ package com.todoroo.astrid.gtasks; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.astrid.gtasks.sync.GtasksSyncV2Provider; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.sync.SyncProviderUtilities; import com.todoroo.astrid.sync.SyncV2BackgroundService; import com.todoroo.astrid.sync.SyncV2Provider; @@ -33,12 +32,10 @@ public class GtasksBackgroundService extends SyncV2BackgroundService { @Override public void onCreate() { super.onCreate(); - StatisticsService.sessionStart(this); } @Override public void onDestroy() { - StatisticsService.sessionStop(this); super.onDestroy(); } diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java index 4387c387f..2ba7c1aea 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/GtasksPreferenceService.java @@ -8,7 +8,6 @@ package com.todoroo.astrid.gtasks; import com.timsu.astrid.R; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.sync.SyncProviderUtilities; /** @@ -58,7 +57,6 @@ public class GtasksPreferenceService extends SyncProviderUtilities { @Override protected void reportLastErrorImpl(String lastError, String type) { - StatisticsService.reportEvent(StatisticsConstants.GTASKS_SYNC_ERROR, "type", type); //$NON-NLS-1$ } } diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java index e271b9a77..6acdcf418 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/auth/GtasksLoginActivity.java @@ -36,7 +36,6 @@ import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.gtasks.GtasksPreferenceService; import com.todoroo.astrid.gtasks.api.GtasksInvoker; import com.todoroo.astrid.service.AstridDependencyInjector; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.SyncV2Service; /** @@ -169,19 +168,16 @@ public class GtasksLoginActivity extends ListActivity { @Override protected void onResume() { super.onResume(); - StatisticsService.sessionStart(this); } @Override protected void onPause() { super.onPause(); - StatisticsService.sessionPause(); } @Override protected void onStop() { super.onStop(); - StatisticsService.sessionStop(this); } private static final int REQUEST_AUTHENTICATE = 0; diff --git a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncV2Provider.java b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncV2Provider.java index e31d3a214..62617a8e3 100644 --- a/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncV2Provider.java +++ b/astrid/plugin-src/com/todoroo/astrid/gtasks/sync/GtasksSyncV2Provider.java @@ -55,7 +55,6 @@ import com.todoroo.astrid.gtasks.auth.GtasksTokenValidator; import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.SyncResultCallbackWrapper.WidgetUpdatingCallbackWrapper; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.sync.SyncResultCallback; @@ -377,7 +376,6 @@ public class GtasksSyncV2Provider extends SyncV2Provider { } else { mergeDates(task.task, local); if(task.task.isCompleted() && !local.isCompleted()) { - StatisticsService.reportEvent(StatisticsConstants.GTASKS_TASK_COMPLETED); } } } else { // Set default importance and reminders for remotely created tasks diff --git a/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java b/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java index f50147e7e..6dbd2b462 100644 --- a/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java +++ b/astrid/plugin-src/com/todoroo/astrid/locale/LocaleEditAlerts.java @@ -28,7 +28,6 @@ import com.todoroo.astrid.api.FilterCategory; import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.twofortyfouram.SharedResources; /** @@ -200,9 +199,6 @@ public final class LocaleEditAlerts extends ListActivity { .setPositiveButton(android.R.string.ok, AddOnActivity.createAddOnClicker(LocaleEditAlerts.this, true)) .show(); - StatisticsService.reportEvent(StatisticsConstants.LOCALE_EDIT_ALERTS_NO_PLUGIN); - } else { - StatisticsService.reportEvent(StatisticsConstants.LOCALE_EDIT_ALERTS); } } @@ -281,14 +277,12 @@ public final class LocaleEditAlerts extends ListActivity { @Override protected void onResume() { super.onResume(); - StatisticsService.sessionStart(this); adapter.registerRecevier(); } @Override protected void onPause() { super.onPause(); - StatisticsService.sessionPause(); adapter.unregisterRecevier(); } @@ -300,7 +294,6 @@ public final class LocaleEditAlerts extends ListActivity { @Override protected void onStop() { super.onStop(); - StatisticsService.sessionStop(this); } /** diff --git a/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java b/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java index 9ad75c4d3..d0e111069 100644 --- a/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java +++ b/astrid/plugin-src/com/todoroo/astrid/notes/EditNoteActivity.java @@ -79,7 +79,6 @@ import com.todoroo.astrid.helper.AsyncImageView; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.timers.TimerActionControlSet.TimerActionListener; import com.todoroo.astrid.utility.ResourceDrawableCache; @@ -546,7 +545,6 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene if (pictureButton != null) { pictureButton.setImageResource(cameraButton); } - StatisticsService.reportEvent(StatisticsConstants.ACTFM_TASK_COMMENT); setUpListAdapter(); for (UpdatesChangedListener l : listeners) { diff --git a/astrid/plugin-src/com/todoroo/astrid/reminders/NotificationFragment.java b/astrid/plugin-src/com/todoroo/astrid/reminders/NotificationFragment.java index b58c2b8a1..266235293 100644 --- a/astrid/plugin-src/com/todoroo/astrid/reminders/NotificationFragment.java +++ b/astrid/plugin-src/com/todoroo/astrid/reminders/NotificationFragment.java @@ -30,7 +30,6 @@ import com.todoroo.astrid.activity.TaskListFragment; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.repeats.RepeatControlSet; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.ui.NumberPicker; /** @@ -62,7 +61,6 @@ public class NotificationFragment extends TaskListFragment { @Override protected void onTaskCompleted(Task item) { - StatisticsService.reportEvent(StatisticsConstants.TASK_COMPLETED_NOTIFICATION); } @Override diff --git a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderDialog.java b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderDialog.java index e7878f7d6..d333aa22e 100644 --- a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderDialog.java +++ b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderDialog.java @@ -49,7 +49,6 @@ import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.User; import com.todoroo.astrid.helper.AsyncImageView; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.tags.TagMemberMetadata; import com.todoroo.astrid.tags.TagService; @@ -86,7 +85,6 @@ public class ReminderDialog extends Dialog { task.setValue(Task.REMINDER_SNOOZE, time); PluginServices.getTaskService().save(task); dismiss(); - StatisticsService.reportEvent(StatisticsConstants.TASK_SNOOZE); } }; final OnTimeSetListener onTimeSet = new OnTimeSetListener() { diff --git a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderPreferences.java b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderPreferences.java index 97ca9718f..f487d7af4 100644 --- a/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderPreferences.java +++ b/astrid/plugin-src/com/todoroo/astrid/reminders/ReminderPreferences.java @@ -58,7 +58,6 @@ public class ReminderPreferences extends TodorooPreferenceActivity { int index = AndroidUtilities.indexOf(r.getStringArray(R.array.EPr_rmd_time_values), (String)value); if (index != -1 && index < r.getStringArray(R.array.EPr_rmd_time).length) { // FIXME this does not fix the underlying cause of the ArrayIndexOutofBoundsException - // https://www.crittercism.com/developers/crash-details/e0886dbfcf9e78a21d9f2e2a385c4c13e2f6ad2132ac24a3fa811144 String setting = r.getStringArray(R.array.EPr_rmd_time)[index]; preference.setSummary(r.getString(R.string.rmd_EPr_rmd_time_desc, setting)); } diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java index c20e74a4e..1bf6c47f3 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java @@ -39,7 +39,6 @@ import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.ThemeService; import com.todoroo.astrid.ui.DateAndTimeDialog; @@ -341,7 +340,6 @@ public class RepeatControlSet extends PopupControlSet { result = ""; //$NON-NLS-1$ } else { if(TextUtils.isEmpty(task.getValue(Task.RECURRENCE))) { - StatisticsService.reportEvent(StatisticsConstants.REPEAT_TASK_CREATE); } RRule rrule = new RRule(); diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java index fed6a72a2..87cf2b3c2 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java @@ -36,7 +36,6 @@ import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.gcal.GCalHelper; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.utility.Flags; @@ -73,9 +72,6 @@ public class RepeatTaskCompleteListener extends BroadcastReceiver { return; } - - StatisticsService.reportEvent(StatisticsConstants.V2_TASK_REPEAT); - long oldDueDate = task.getValue(Task.DUE_DATE); long repeatUntil = task.getValue(Task.REPEAT_UNTIL); diff --git a/astrid/plugin-src/com/todoroo/astrid/tags/reusable/FeaturedTaskListFragment.java b/astrid/plugin-src/com/todoroo/astrid/tags/reusable/FeaturedTaskListFragment.java index b28439bc5..c7c21451f 100644 --- a/astrid/plugin-src/com/todoroo/astrid/tags/reusable/FeaturedTaskListFragment.java +++ b/astrid/plugin-src/com/todoroo/astrid/tags/reusable/FeaturedTaskListFragment.java @@ -29,7 +29,6 @@ import com.todoroo.astrid.data.TagData; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.helper.AsyncImageView; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TagDataService; import com.todoroo.astrid.tags.TagFilterExposer; import com.todoroo.astrid.tags.TagService.Tag; @@ -130,7 +129,6 @@ public class FeaturedTaskListFragment extends TagViewFragment { return; } - StatisticsService.reportEvent(StatisticsConstants.FEATURED_LIST_CLONED); final String localName = tagData.getValue(TagData.NAME) + " " + getString(R.string.actfm_feat_list_suffix); //$NON-NLS-1$ TagData clone = new TagData(); TodorooCursor existing = tagDataService.query(Query.select(TagData.PROPERTIES) diff --git a/astrid/plugin-src/com/todoroo/astrid/timers/TimerPlugin.java b/astrid/plugin-src/com/todoroo/astrid/timers/TimerPlugin.java index 272656906..41a6fe94f 100644 --- a/astrid/plugin-src/com/todoroo/astrid/timers/TimerPlugin.java +++ b/astrid/plugin-src/com/todoroo/astrid/timers/TimerPlugin.java @@ -25,7 +25,6 @@ import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.utility.Constants; public class TimerPlugin extends BroadcastReceiver { @@ -62,7 +61,6 @@ public class TimerPlugin extends BroadcastReceiver { if(start) { if(task.getValue(Task.TIMER_START) == 0) { task.setValue(Task.TIMER_START, DateUtilities.now()); - StatisticsService.reportEvent(StatisticsConstants.TIMER_START); } } else { if(task.getValue(Task.TIMER_START) > 0) { @@ -70,7 +68,6 @@ public class TimerPlugin extends BroadcastReceiver { task.setValue(Task.TIMER_START, 0L); task.setValue(Task.ELAPSED_SECONDS, task.getValue(Task.ELAPSED_SECONDS) + newElapsed); - StatisticsService.reportEvent(StatisticsConstants.TIMER_STOP); } } PluginServices.getTaskService().save(task); diff --git a/astrid/proguard.cfg b/astrid/proguard.cfg index 5d0c546b8..b9bf134da 100644 --- a/astrid/proguard.cfg +++ b/astrid/proguard.cfg @@ -41,12 +41,6 @@ -keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.preference.Preference --keep public class com.crittercism.* - --keepclassmembers public class com.crittercism.* { - *; -} - -keep class com.facebook.** { *; } diff --git a/astrid/res/values/keys.xml b/astrid/res/values/keys.xml index 9ddc4b7a1..e8fd2ddde 100644 --- a/astrid/res/values/keys.xml +++ b/astrid/res/values/keys.xml @@ -412,11 +412,6 @@ actfm_sync_freq - - - - statistics - showed_add_task_help diff --git a/astrid/res/xml/preferences_misc.xml b/astrid/res/xml/preferences_misc.xml index 3185af34b..281cfda1b 100644 --- a/astrid/res/xml/preferences_misc.xml +++ b/astrid/res/xml/preferences_misc.xml @@ -24,11 +24,5 @@ - - - \ No newline at end of file diff --git a/astrid/src/com/todoroo/astrid/activity/AstridActivity.java b/astrid/src/com/todoroo/astrid/activity/AstridActivity.java index a534a8e66..03b396526 100644 --- a/astrid/src/com/todoroo/astrid/activity/AstridActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/AstridActivity.java @@ -44,7 +44,6 @@ import com.todoroo.astrid.data.TagData; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.subtasks.SubtasksHelper; import com.todoroo.astrid.ui.DateChangedAlerts; import com.todoroo.astrid.ui.QuickAddBar; @@ -113,7 +112,6 @@ public class AstridActivity extends SherlockFragmentActivity DependencyInjectionService.getInstance().inject(this); super.onCreate(savedInstanceState); ContextManager.setContext(this); - StatisticsService.sessionStart(this); new StartupService().onStartupApplication(this); } @@ -132,14 +130,12 @@ public class AstridActivity extends SherlockFragmentActivity protected void onPause() { super.onPause(); - StatisticsService.sessionPause(); AndroidUtilities.tryUnregisterReceiver(this, repeatConfirmationReceiver); } @Override protected void onStop() { super.onStop(); - StatisticsService.sessionStop(this); } /** @@ -152,7 +148,6 @@ public class AstridActivity extends SherlockFragmentActivity } if (item instanceof SearchFilter) { onSearchRequested(); - StatisticsService.reportEvent(StatisticsConstants.FILTER_SEARCH); return false; } else { // If showing both fragments, directly update the tasklist-fragment @@ -169,7 +164,6 @@ public class AstridActivity extends SherlockFragmentActivity // no animation for dualpane-layout AndroidUtilities.callOverridePendingTransition(this, 0, 0); - StatisticsService.reportEvent(StatisticsConstants.FILTER_LIST); return true; } else if(item instanceof IntentFilter) { try { diff --git a/astrid/src/com/todoroo/astrid/activity/EditPreferences.java b/astrid/src/com/todoroo/astrid/activity/EditPreferences.java index 2dc68f7b9..c16e8b96e 100644 --- a/astrid/src/com/todoroo/astrid/activity/EditPreferences.java +++ b/astrid/src/com/todoroo/astrid/activity/EditPreferences.java @@ -31,7 +31,6 @@ import android.preference.PreferenceScreen; import android.text.TextUtils; import android.widget.Toast; -import com.crittercism.app.Crittercism; import com.timsu.astrid.R; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; @@ -56,7 +55,6 @@ import com.todoroo.astrid.service.AddOnService; import com.todoroo.astrid.service.MarketStrategy.AmazonMarketStrategy; import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.sync.SyncProviderPreferences; import com.todoroo.astrid.ui.ContactListAdapter; @@ -136,7 +134,6 @@ public class EditPreferences extends TodorooPreferenceActivity { @Override public void onClick(DialogInterface dialog, int which) { spec.resetDefaults(); - StatisticsService.reportEvent(statistic); setResult(RESULT_CODE_PERFORMANCE_PREF_CHANGED); finish(); } @@ -288,7 +285,6 @@ public class EditPreferences extends TodorooPreferenceActivity { boolean hasPowerPack = addOnService.hasPowerPack(); findPreference(getString(R.string.p_files_dir)).setEnabled(ActFmPreferenceService.isPremiumUser()); findPreference(getString(R.string.p_voiceRemindersEnabled)).setEnabled(hasPowerPack); - findPreference(getString(R.string.p_statistics)).setEnabled(hasPowerPack); } /** Show about dialog */ @@ -517,8 +513,6 @@ public class EditPreferences extends TodorooPreferenceActivity { @Override public boolean onPreferenceChange(Preference p, Object newValue) { String valueString = newValue.toString(); - StatisticsService.reportEvent(StatisticsConstants.PREF_CHANGED_PREFIX + "row-style", //$NON-NLS-1$ - "changed-to", valueString); //$NON-NLS-1$ Preference notes = findPreference(getString(R.string.p_showNotes)); Preference fullTitle = findPreference(getString(R.string.p_fullTaskTitle)); try { @@ -588,10 +582,6 @@ public class EditPreferences extends TodorooPreferenceActivity { dir = r.getString(R.string.p_files_dir_desc_default); } preference.setSummary(r.getString(R.string.p_files_dir_desc, dir)); - } - else if (booleanPreference(preference, value, R.string.p_statistics, - R.string.EPr_statistics_desc_disabled, R.string.EPr_statistics_desc_enabled)) { - ; } else if (booleanPreference(preference, value, R.string.p_field_missed_calls, R.string.MCA_missed_calls_pref_desc_disabled, R.string.MCA_missed_calls_pref_desc_enabled)) { ; @@ -718,28 +708,10 @@ public class EditPreferences extends TodorooPreferenceActivity { }); } - findPreference(getString(R.string.p_statistics)).setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Boolean value = (Boolean) newValue; - try { - if (!value.booleanValue()) { - Crittercism.setOptOutStatus(true); - } else { - Crittercism.setOptOutStatus(false); - } - } catch (NullPointerException e) { - return false; - } - return true; - } - }); - findPreference(getString(R.string.p_showNotes)).setOnPreferenceChangeListener(new OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { updatePreferences(preference, newValue); - StatisticsService.reportEvent(StatisticsConstants.PREF_SHOW_NOTES_IN_ROW, "enabled", newValue.toString()); //$NON-NLS-1$ return true; } }); @@ -748,7 +720,6 @@ public class EditPreferences extends TodorooPreferenceActivity { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { updatePreferences(preference, newValue); - StatisticsService.reportEvent(StatisticsConstants.PREF_CHANGED_PREFIX + "full-title", "full-title", newValue.toString()); //$NON-NLS-1$ //$NON-NLS-2$ return true; } }); @@ -817,7 +788,6 @@ public class EditPreferences extends TodorooPreferenceActivity { @Override protected void onPause() { - StatisticsService.sessionPause(); super.onPause(); } @@ -829,12 +799,10 @@ public class EditPreferences extends TodorooPreferenceActivity { @Override protected void onResume() { super.onResume(); - StatisticsService.sessionStart(this); } @Override protected void onStop() { - StatisticsService.sessionStop(this); super.onStop(); } diff --git a/astrid/src/com/todoroo/astrid/activity/Eula.java b/astrid/src/com/todoroo/astrid/activity/Eula.java index 45a7cc848..58dec29b4 100644 --- a/astrid/src/com/todoroo/astrid/activity/Eula.java +++ b/astrid/src/com/todoroo/astrid/activity/Eula.java @@ -26,7 +26,6 @@ import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; /** @@ -110,7 +109,6 @@ public final class Eula { ((EulaCallback)activity).eulaAccepted(); } Preferences.setBoolean(PREFERENCE_EULA_ACCEPTED, true); - StatisticsService.reportEvent(StatisticsConstants.EULA_ACCEPTED); } private static void refuse(Activity activity) { diff --git a/astrid/src/com/todoroo/astrid/activity/FilterListFragment.java b/astrid/src/com/todoroo/astrid/activity/FilterListFragment.java index 22a2bf59e..f36a576c3 100644 --- a/astrid/src/com/todoroo/astrid/activity/FilterListFragment.java +++ b/astrid/src/com/todoroo/astrid/activity/FilterListFragment.java @@ -52,7 +52,6 @@ import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.FilterWithUpdate; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.tags.TagsPlugin; import com.todoroo.astrid.utility.AstridPreferences; @@ -186,14 +185,12 @@ public class FilterListFragment extends SherlockListFragment { @Override public void onStop() { - StatisticsService.sessionStop(getActivity()); super.onStop(); } @Override public void onResume() { super.onResume(); - StatisticsService.sessionStart(getActivity()); if(adapter != null) { adapter.registerRecevier(); } @@ -215,7 +212,6 @@ public class FilterListFragment extends SherlockListFragment { @Override public void onPause() { - StatisticsService.sessionPause(); super.onPause(); if(adapter != null) { adapter.unregisterRecevier(); diff --git a/astrid/src/com/todoroo/astrid/activity/FilterShortcutActivity.java b/astrid/src/com/todoroo/astrid/activity/FilterShortcutActivity.java index bee7dd4fd..048bfd4dd 100644 --- a/astrid/src/com/todoroo/astrid/activity/FilterShortcutActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/FilterShortcutActivity.java @@ -17,7 +17,6 @@ import com.timsu.astrid.R; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.api.Filter; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.ThemeService; @SuppressWarnings("nls") @@ -79,14 +78,12 @@ public class FilterShortcutActivity extends ListActivity { @Override protected void onResume() { super.onResume(); - StatisticsService.sessionStart(this); adapter.registerRecevier(); } @Override protected void onPause() { super.onPause(); - StatisticsService.sessionPause(); adapter.unregisterRecevier(); } @@ -98,7 +95,6 @@ public class FilterShortcutActivity extends ListActivity { @Override protected void onStop() { super.onStop(); - StatisticsService.sessionStop(this); } } diff --git a/astrid/src/com/todoroo/astrid/activity/ShareActivity.java b/astrid/src/com/todoroo/astrid/activity/ShareActivity.java index ec72e82ae..40e7b7f8d 100644 --- a/astrid/src/com/todoroo/astrid/activity/ShareActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/ShareActivity.java @@ -16,7 +16,6 @@ import com.actionbarsherlock.app.SherlockFragmentActivity; import com.actionbarsherlock.view.MenuItem; import com.timsu.astrid.R; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.ThemeService; public class ShareActivity extends SherlockFragmentActivity { @@ -45,7 +44,6 @@ public class ShareActivity extends SherlockFragmentActivity { setUpTextView(google, getString(R.string.share_with_google), "https://plus.google.com/116404018347675245869", "google"); //$NON-NLS-1$ //$NON-NLS-2$ setupText(); - StatisticsService.reportEvent(StatisticsConstants.SHARE_PAGE_VIEWED); } private void setUpTextView(TextView tv, String text, final String url, final String buttonId) { @@ -53,7 +51,6 @@ public class ShareActivity extends SherlockFragmentActivity { ((View) tv.getParent()).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - StatisticsService.reportEvent(StatisticsConstants.SHARE_BUTTON_CLICKED, "button", buttonId); //$NON-NLS-1$ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); } diff --git a/astrid/src/com/todoroo/astrid/activity/TaskEditFragment.java b/astrid/src/com/todoroo/astrid/activity/TaskEditFragment.java index 00428b436..3811159a1 100755 --- a/astrid/src/com/todoroo/astrid/activity/TaskEditFragment.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskEditFragment.java @@ -93,7 +93,6 @@ import com.todoroo.astrid.opencrx.OpencrxCoreUtils; import com.todoroo.astrid.reminders.Notifications; import com.todoroo.astrid.repeats.RepeatControlSet; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.ThemeService; import com.todoroo.astrid.tags.TagsControlSet; @@ -786,12 +785,9 @@ ViewPager.OnPageChangeListener, EditNoteActivity.UpdatesChangedListener { } if (model.getValue(Task.TITLE).length() == 0) { - StatisticsService.reportEvent(StatisticsConstants.CREATE_TASK); // set deletion date until task gets a title model.setValue(Task.DELETION_DATE, DateUtilities.now()); - } else { - StatisticsService.reportEvent(StatisticsConstants.EDIT_TASK); } setIsNewTask(model.getValue(Task.TITLE).length() == 0); @@ -1340,7 +1336,6 @@ ViewPager.OnPageChangeListener, EditNoteActivity.UpdatesChangedListener { @Override public void onPause() { super.onPause(); - StatisticsService.sessionPause(); if (shouldSaveState) { save(true); @@ -1350,7 +1345,6 @@ ViewPager.OnPageChangeListener, EditNoteActivity.UpdatesChangedListener { @Override public void onResume() { super.onResume(); - StatisticsService.sessionStart(getActivity()); populateFields(); } @@ -1414,7 +1408,6 @@ ViewPager.OnPageChangeListener, EditNoteActivity.UpdatesChangedListener { @Override public void onStop() { super.onStop(); - StatisticsService.sessionStop(getActivity()); } private void adjustInfoPopovers() { diff --git a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java index c4b038b8a..0dc2a2d47 100644 --- a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java @@ -59,7 +59,6 @@ import com.todoroo.astrid.data.Task; import com.todoroo.astrid.people.PeopleFilterMode; import com.todoroo.astrid.people.PersonViewFragment; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.ThemeService; import com.todoroo.astrid.service.abtesting.ABTestEventReportingService; import com.todoroo.astrid.tags.TagFilterExposer; @@ -860,22 +859,16 @@ public class TaskListActivity extends AstridActivity implements MainMenuListener switch (getIntent().getIntExtra(TOKEN_SOURCE, Constants.SOURCE_DEFAULT)) { case Constants.SOURCE_NOTIFICATION: - StatisticsService.reportEvent(StatisticsConstants.LAUNCH_FROM_NOTIFICATION); break; case Constants.SOURCE_OTHER: - StatisticsService.reportEvent(StatisticsConstants.LAUNCH_FROM_OTHER); break; case Constants.SOURCE_PPWIDGET: - StatisticsService.reportEvent(StatisticsConstants.LAUNCH_FROM_PPW); break; case Constants.SOURCE_WIDGET: - StatisticsService.reportEvent(StatisticsConstants.LAUNCH_FROM_WIDGET); break; case Constants.SOURCE_C2DM: - StatisticsService.reportEvent(StatisticsConstants.LAUNCH_FROM_C2DM); break; case Constants.SOURCE_REENGAGEMENT: - StatisticsService.reportEvent(StatisticsConstants.LAUNCH_FROM_REENGAGEMENT); } getIntent().putExtra(TOKEN_SOURCE, Constants.SOURCE_DEFAULT); // Only report source once } diff --git a/astrid/src/com/todoroo/astrid/activity/TaskListFragment.java b/astrid/src/com/todoroo/astrid/activity/TaskListFragment.java index c9258d283..9ae480249 100644 --- a/astrid/src/com/todoroo/astrid/activity/TaskListFragment.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskListFragment.java @@ -97,7 +97,6 @@ import com.todoroo.astrid.service.AddOnService; import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TagDataService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.service.ThemeService; @@ -1198,9 +1197,7 @@ public class TaskListFragment extends SherlockListFragment implements OnSortSele */ protected void onTaskCompleted(Task item) { if (isInbox) { - StatisticsService.reportEvent(StatisticsConstants.TASK_COMPLETED_INBOX); } else { - StatisticsService.reportEvent(StatisticsConstants.TASK_COMPLETED_FILTER); } } @@ -1348,7 +1345,6 @@ public class TaskListFragment extends SherlockListFragment implements OnSortSele Activity activity = getActivity(); switch(id) { case MENU_SORT_ID: - StatisticsService.reportEvent(StatisticsConstants.TLA_MENU_SORT); if (activity != null) { AlertDialog dialog = SortSelectionActivity.createDialog( getActivity(), hasDraggableOption(), this, sortFlags, sortSort); @@ -1356,7 +1352,6 @@ public class TaskListFragment extends SherlockListFragment implements OnSortSele } return true; case MENU_SYNC_ID: - StatisticsService.reportEvent(StatisticsConstants.TLA_MENU_SYNC); syncActionHelper.performSyncAction(); return true; case MENU_ADDON_INTENT_ID: @@ -1466,7 +1461,6 @@ public class TaskListFragment extends SherlockListFragment implements OnSortSele return; } - StatisticsService.reportEvent(StatisticsConstants.TLA_MENU_SETTINGS); Intent intent = new Intent(activity, EditPreferences.class); startActivityForResult(intent, ACTIVITY_SETTINGS); } diff --git a/astrid/src/com/todoroo/astrid/adapter/AddOnAdapter.java b/astrid/src/com/todoroo/astrid/adapter/AddOnAdapter.java index 1b2276782..fec7d22ef 100644 --- a/astrid/src/com/todoroo/astrid/adapter/AddOnAdapter.java +++ b/astrid/src/com/todoroo/astrid/adapter/AddOnAdapter.java @@ -25,7 +25,6 @@ import android.widget.Toast; import com.timsu.astrid.R; import com.todoroo.astrid.data.AddOn; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.utility.Constants; /** @@ -59,7 +58,6 @@ public class AddOnAdapter extends ArrayAdapter { if(buttonTag != null) { try { activity.startActivity(buttonTag.intent); - StatisticsService.reportEvent("addon-" + buttonTag.event); //$NON-NLS-1$ } catch (ActivityNotFoundException e) { Toast.makeText(activity, R.string.market_unavailable, Toast.LENGTH_LONG).show(); } diff --git a/astrid/src/com/todoroo/astrid/dao/Database.java b/astrid/src/com/todoroo/astrid/dao/Database.java index 8dcc78906..0b243cb93 100644 --- a/astrid/src/com/todoroo/astrid/dao/Database.java +++ b/astrid/src/com/todoroo/astrid/dao/Database.java @@ -9,7 +9,6 @@ import android.database.sqlite.SQLiteException; import android.text.TextUtils; import android.util.Log; -import com.crittercism.app.Crittercism; import com.todoroo.andlib.data.AbstractDatabase; import com.todoroo.andlib.data.AbstractModel; import com.todoroo.andlib.data.Property; @@ -163,7 +162,6 @@ public class Database extends AbstractDatabase { } @Override - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="SF_SWITCH_FALLTHROUGH") protected synchronized boolean onUpgrade(int oldVersion, int newVersion) { SqlConstructorVisitor visitor = new SqlConstructorVisitor(); switch(oldVersion) { @@ -416,7 +414,6 @@ public class Database extends AbstractDatabase { database.execSQL(sql); } catch (SQLiteException e) { Log.e("astrid", "SQL Error: " + sql, e); - Crittercism.logHandledException(e); } } diff --git a/astrid/src/com/todoroo/astrid/dao/MetadataDao.java b/astrid/src/com/todoroo/astrid/dao/MetadataDao.java index 6ed9ad15b..a437f4f37 100644 --- a/astrid/src/com/todoroo/astrid/dao/MetadataDao.java +++ b/astrid/src/com/todoroo/astrid/dao/MetadataDao.java @@ -33,7 +33,6 @@ import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.TaskOutstanding; import com.todoroo.astrid.provider.Astrid2TaskProvider; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.tags.TaskToTagMetadata; import com.todoroo.astrid.utility.AstridPreferences; @@ -48,7 +47,6 @@ public class MetadataDao extends DatabaseDao { @Autowired private Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public MetadataDao() { super(Metadata.class); DependencyInjectionService.getInstance().inject(this); @@ -176,7 +174,6 @@ public class MetadataDao extends DatabaseDao { if(Preferences.getBoolean(AstridPreferences.P_FIRST_LIST, true)) { if (state && item.containsNonNullValue(Metadata.KEY) && item.getValue(Metadata.KEY).equals(TaskToTagMetadata.KEY)) { - StatisticsService.reportEvent(StatisticsConstants.USER_FIRST_LIST); Preferences.setBoolean(AstridPreferences.P_FIRST_LIST, false); } } diff --git a/astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java b/astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java index fecb3f009..5f571fa7e 100644 --- a/astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java +++ b/astrid/src/com/todoroo/astrid/dao/StoreObjectDao.java @@ -22,7 +22,6 @@ public class StoreObjectDao extends DatabaseDao { @Autowired private Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public StoreObjectDao() { super(StoreObject.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/dao/TagDataDao.java b/astrid/src/com/todoroo/astrid/dao/TagDataDao.java index d99dc72dc..2d87df923 100644 --- a/astrid/src/com/todoroo/astrid/dao/TagDataDao.java +++ b/astrid/src/com/todoroo/astrid/dao/TagDataDao.java @@ -21,7 +21,6 @@ public class TagDataDao extends RemoteModelDao { @Autowired Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public TagDataDao() { super(TagData.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/dao/TagMetadataDao.java b/astrid/src/com/todoroo/astrid/dao/TagMetadataDao.java index c0b19dc1e..7cb8f7928 100644 --- a/astrid/src/com/todoroo/astrid/dao/TagMetadataDao.java +++ b/astrid/src/com/todoroo/astrid/dao/TagMetadataDao.java @@ -48,7 +48,6 @@ public class TagMetadataDao extends DatabaseDao { @Autowired private Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public TagMetadataDao() { super(TagMetadata.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/dao/TaskAttachmentDao.java b/astrid/src/com/todoroo/astrid/dao/TaskAttachmentDao.java index 57bc87b00..76a2790c9 100644 --- a/astrid/src/com/todoroo/astrid/dao/TaskAttachmentDao.java +++ b/astrid/src/com/todoroo/astrid/dao/TaskAttachmentDao.java @@ -33,7 +33,6 @@ public class TaskAttachmentDao extends RemoteModelDao { @Autowired Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public TaskAttachmentDao() { super(TaskAttachment.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/dao/TaskDao.java b/astrid/src/com/todoroo/astrid/dao/TaskDao.java index 39384f484..750f57329 100644 --- a/astrid/src/com/todoroo/astrid/dao/TaskDao.java +++ b/astrid/src/com/todoroo/astrid/dao/TaskDao.java @@ -43,7 +43,6 @@ public class TaskDao extends RemoteModelDao { @Autowired private Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public TaskDao() { super(Task.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/dao/TaskListMetadataDao.java b/astrid/src/com/todoroo/astrid/dao/TaskListMetadataDao.java index ad5b043a1..ab8bba620 100644 --- a/astrid/src/com/todoroo/astrid/dao/TaskListMetadataDao.java +++ b/astrid/src/com/todoroo/astrid/dao/TaskListMetadataDao.java @@ -26,7 +26,6 @@ public class TaskListMetadataDao extends RemoteModelDao { @Autowired Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public TaskListMetadataDao() { super(TaskListMetadata.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/dao/UpdateDao.java b/astrid/src/com/todoroo/astrid/dao/UpdateDao.java index a177ad32a..1560c232c 100644 --- a/astrid/src/com/todoroo/astrid/dao/UpdateDao.java +++ b/astrid/src/com/todoroo/astrid/dao/UpdateDao.java @@ -21,7 +21,6 @@ public class UpdateDao extends RemoteModelDao { @Autowired Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public UpdateDao() { super(Update.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/dao/UserDao.java b/astrid/src/com/todoroo/astrid/dao/UserDao.java index 7416ddbd3..c8065c629 100644 --- a/astrid/src/com/todoroo/astrid/dao/UserDao.java +++ b/astrid/src/com/todoroo/astrid/dao/UserDao.java @@ -13,7 +13,6 @@ import com.todoroo.astrid.data.User; public class UserDao extends RemoteModelDao { @Autowired Database database; - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="UR_UNINIT_READ") public UserDao() { super(User.class); DependencyInjectionService.getInstance().inject(this); diff --git a/astrid/src/com/todoroo/astrid/provider/Astrid2TaskProvider.java b/astrid/src/com/todoroo/astrid/provider/Astrid2TaskProvider.java index 2b3ab76e5..798b06bdb 100644 --- a/astrid/src/com/todoroo/astrid/provider/Astrid2TaskProvider.java +++ b/astrid/src/com/todoroo/astrid/provider/Astrid2TaskProvider.java @@ -30,7 +30,6 @@ import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.tags.TagService.Tag; @@ -273,7 +272,6 @@ public class Astrid2TaskProvider extends ContentProvider { task.setValue(Task.COMPLETION_DATE, values.getAsBoolean(COMPLETED) ? DateUtilities.now() : 0); if(task.isCompleted()) { - StatisticsService.reportEvent(StatisticsConstants.TASK_COMPLETED_API2); } } diff --git a/astrid/src/com/todoroo/astrid/service/StartupService.java b/astrid/src/com/todoroo/astrid/service/StartupService.java index 672ed733b..56112d972 100644 --- a/astrid/src/com/todoroo/astrid/service/StartupService.java +++ b/astrid/src/com/todoroo/astrid/service/StartupService.java @@ -25,7 +25,6 @@ import android.media.AudioManager; import android.util.Log; import android.widget.Toast; -import com.crittercism.app.Crittercism; import com.timsu.astrid.R; import com.todoroo.andlib.data.DatabaseDao.ModelUpdateListener; import com.todoroo.andlib.data.TodorooCursor; @@ -149,10 +148,6 @@ public class StartupService { // sets up context manager ContextManager.setContext(context); - if(!StatisticsService.dontCollectStatistics()) { - Crittercism.init(context.getApplicationContext(), Constants.CRITTERCISM_APP_ID); - } - try { database.openForWriting(); checkForMissingColumns(); @@ -344,9 +339,7 @@ public class StartupService { File[] children = directory.listFiles(); AndroidUtilities.sortFilesByDateDesc(children); if(children.length > 0) { - StatisticsService.sessionStart(context); TasksXmlImporter.importTasks(context, children[0].getAbsolutePath(), null); - StatisticsService.reportEvent(StatisticsConstants.LOST_TASKS_RESTORED); } } } catch (Exception e) { @@ -359,7 +352,6 @@ public class StartupService { private void checkForSubtasksUse() { if (!Preferences.getBoolean(PREF_SUBTASKS_CHECK, false)) { if (taskService.countTasks() > 3) { - StatisticsService.reportEvent(StatisticsConstants.SUBTASKS_HAS_TASKS); checkMetadataStat(Criterion.and(MetadataCriteria.withKey(SubtasksMetadata.METADATA_KEY), SubtasksMetadata.ORDER.gt(0)), StatisticsConstants.SUBTASKS_ORDER_USED); checkMetadataStat(Criterion.and(MetadataCriteria.withKey(SubtasksMetadata.METADATA_KEY), @@ -377,7 +369,6 @@ public class StartupService { if (!Preferences.getBoolean(PREF_SWIPE_CHECK, false)) { if (Preferences.getBoolean(R.string.p_swipe_lists_enabled, false) && Preferences.getBoolean(TaskListFragmentPager.PREF_SHOWED_SWIPE_HELPER, false)) { - StatisticsService.reportEvent(StatisticsConstants.SWIPE_USED); } Preferences.setBoolean(PREF_SWIPE_CHECK, true); } @@ -388,7 +379,6 @@ public class StartupService { private void checkForVoiceRemindersUse() { if (!Preferences.getBoolean(PREF_VOICE_REMINDERS_CHECK, false)) { if (Preferences.getBoolean(R.string.p_voiceRemindersEnabled, false)) { - StatisticsService.reportEvent(StatisticsConstants.VOICE_REMINDERS_ENABLED); Preferences.setBoolean(PREF_VOICE_REMINDERS_CHECK, true); } } @@ -398,7 +388,6 @@ public class StartupService { TodorooCursor sort = metadataService.query(Query.select(Metadata.ID).where(criterion).limit(1)); try { if (sort.getCount() > 0) { - StatisticsService.reportEvent(statistic); } } finally { sort.close(); diff --git a/astrid/src/com/todoroo/astrid/service/StatisticsConstants.java b/astrid/src/com/todoroo/astrid/service/StatisticsConstants.java index 6a7fd28cf..0ab222864 100644 --- a/astrid/src/com/todoroo/astrid/service/StatisticsConstants.java +++ b/astrid/src/com/todoroo/astrid/service/StatisticsConstants.java @@ -42,7 +42,6 @@ public class StatisticsConstants { public static final String TLA_MENU_SETTINGS = "tla-menu-settings"; public static final String TLA_MENU_SORT = "tla-menu-sort"; public static final String TLA_MENU_SYNC = "tla-menu-sync"; - public static final String TLA_CRITTERCISM = "tla-crittercism"; public static final String TLA_MENU_HELP = "tla-menu-help"; public static final String V2_TASK_REPEAT = "v2-task-repeat"; public static final String TASK_COMPLETED_INBOX = "task-completed-inbox"; diff --git a/astrid/src/com/todoroo/astrid/service/StatisticsService.java b/astrid/src/com/todoroo/astrid/service/StatisticsService.java deleted file mode 100644 index 951bb71db..000000000 --- a/astrid/src/com/todoroo/astrid/service/StatisticsService.java +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2012 Todoroo Inc - * - * See the file "LICENSE" for the full license governing this code. - */ - -package com.todoroo.astrid.service; - -import java.util.HashMap; - -import android.app.Activity; -import android.content.Context; - -import com.localytics.android.LocalyticsSession; -import com.timsu.astrid.R; -import com.todoroo.andlib.utility.Preferences; -import com.todoroo.astrid.utility.Constants; - -public class StatisticsService { - - private static LocalyticsSession localyticsSession; - - /** - * Indicate session started - * - * @param context - */ - public static void sessionStart(Context context) { - if(dontCollectStatistics()) { - return; - } - - if(localyticsSession != null) { - localyticsSession.open(); // Multiple calls to open are ok, we just need to make sure it gets reopened after pause - } else { - localyticsSession = new LocalyticsSession(context.getApplicationContext(), - Constants.LOCALYTICS_KEY); - localyticsSession.open(); - localyticsSession.upload(); - } - - if (context instanceof Activity) { - localyticsSession.tagScreen(context.getClass().getSimpleName()); - } - } - - /** - * Indicate session ended - * - * @param context - */ - public static void sessionStop(Context context) { - if(dontCollectStatistics()) { - return; - } - - if(localyticsSession != null) { - localyticsSession.upload(); - } - } - - /** - * Indicate session was paused - */ - public static void sessionPause() { - if(dontCollectStatistics()) { - return; - } - - if(localyticsSession != null) { - localyticsSession.close(); - } - } - - /** - * Indicates an error occurred - * @param name - * @param message - * @param trace - */ - public static void reportError(String name, String message, String trace) { - // no reports yet - } - - /** - * Indicates an event should be reported - * @param event - */ - public static void reportEvent(String event, String... attributes) { - if(dontCollectStatistics()) { - return; - } - - if(localyticsSession != null) { - if(attributes.length > 0) { - HashMap attrMap = new HashMap(); - for(int i = 1; i < attributes.length; i += 2) { - if(attributes[i] != null) { - attrMap.put(attributes[i - 1], attributes[i]); - } - } - localyticsSession.tagEvent(event, attrMap); - } else { - localyticsSession.tagEvent(event); - } - } - } - - public static boolean dontCollectStatistics() { - return !Preferences.getBoolean(R.string.p_statistics, true); - } - -} diff --git a/astrid/src/com/todoroo/astrid/service/TaskService.java b/astrid/src/com/todoroo/astrid/service/TaskService.java index 197a83d7c..234704296 100644 --- a/astrid/src/com/todoroo/astrid/service/TaskService.java +++ b/astrid/src/com/todoroo/astrid/service/TaskService.java @@ -148,14 +148,11 @@ public class TaskService { long diff = DateUtilities.now() - reminderLast; if (diff > 0 && diff < DateUtilities.ONE_DAY) { // within one day of last reminder - StatisticsService.reportEvent(StatisticsConstants.TASK_COMPLETED_ONE_DAY, "social", socialReminder); //$NON-NLS-1$ } if (diff > 0 && diff < DateUtilities.ONE_WEEK) { // within one week of last reminder - StatisticsService.reportEvent(StatisticsConstants.TASK_COMPLETED_ONE_WEEK, "social", socialReminder); //$NON-NLS-1$ } } - StatisticsService.reportEvent(StatisticsConstants.TASK_COMPLETED_V2); } else { item.setValue(Task.COMPLETION_DATE, 0L); } diff --git a/astrid/src/com/todoroo/astrid/service/abtesting/ABTestEventReportingService.java b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestEventReportingService.java index 50ef1ec63..1858615e7 100644 --- a/astrid/src/com/todoroo/astrid/service/abtesting/ABTestEventReportingService.java +++ b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestEventReportingService.java @@ -25,7 +25,6 @@ import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.dao.ABTestEventDao; import com.todoroo.astrid.data.ABTestEvent; import com.todoroo.astrid.service.StartupService; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; /** @@ -89,55 +88,11 @@ public final class ABTestEventReportingService { private void pushAllUnreportedABTestEvents() { synchronized(ABTestEventReportingService.class) { - if (StatisticsService.dontCollectStatistics()) { - return; - } - final TodorooCursor unreported = abTestEventDao.query(Query.select(ABTestEvent.PROPERTIES) - .where(ABTestEvent.REPORTED.eq(0)) - .orderBy(Order.asc(ABTestEvent.TEST_NAME), Order.asc(ABTestEvent.TIME_INTERVAL))); - if (unreported.getCount() > 0) { - try { - JSONArray payload = jsonArrayFromABTestEvents(unreported); - abTestInvoker.post(ABTestInvoker.AB_RETENTION_METHOD, payload); - ABTestEvent model = new ABTestEvent(); - for (unreported.moveToFirst(); !unreported.isAfterLast(); unreported.moveToNext()) { - model.readFromCursor(unreported); - model.setValue(ABTestEvent.REPORTED, 1); - abTestEventDao.saveExisting(model); - } - } catch (JSONException e) { - handleException(e); - } catch (IOException e) { - handleException(e); - } finally { - unreported.close(); - } - } } } private void reportUserActivation() { synchronized (ABTestEventReportingService.class) { - if (StatisticsService.dontCollectStatistics()) { - return; - } - if (Preferences.getBoolean(PREF_REPORTED_ACTIVATION, false) || !taskService.getUserActivationStatus()) { - return; - } - - final TodorooCursor variants = abTestEventDao.query(Query.select(ABTestEvent.PROPERTIES) - .groupBy(ABTestEvent.TEST_NAME)); - try { - JSONArray payload = jsonArrayForActivationAnalytics(variants); - abTestInvoker.post(ABTestInvoker.AB_ACTIVATION_METHOD, payload); - Preferences.setBoolean(PREF_REPORTED_ACTIVATION, true); - } catch (JSONException e) { - handleException(e); - } catch (IOException e) { - handleException(e); - } finally { - variants.close(); - } } } diff --git a/astrid/src/com/todoroo/astrid/service/abtesting/ABTestInvoker.java b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestInvoker.java index f69811aea..4a14d7292 100644 --- a/astrid/src/com/todoroo/astrid/service/abtesting/ABTestInvoker.java +++ b/astrid/src/com/todoroo/astrid/service/abtesting/ABTestInvoker.java @@ -24,7 +24,6 @@ import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.RestClient; import com.todoroo.andlib.utility.Preferences; -import com.todoroo.astrid.service.StatisticsService; /** * Invoker for communicating with the Astrid Analytics server @@ -51,21 +50,6 @@ public class ABTestInvoker { } public void reportAcquisition() { - if (!Preferences.getBoolean(PREF_REPORTED_ACQUISITION, false) && - !StatisticsService.dontCollectStatistics()) { - new Thread(new Runnable() { - @Override - public void run() { - try { - HttpEntity postData = createPostData(null); - restClient.post(URL + ACQUISITION_METHOD, postData); - Preferences.setBoolean(PREF_REPORTED_ACQUISITION, true); - } catch (IOException e) { - // Ignored - } - } - }).start(); - } } /** diff --git a/astrid/src/com/todoroo/astrid/ui/QuickAddBar.java b/astrid/src/com/todoroo/astrid/ui/QuickAddBar.java index bd07363bd..0a45b2d33 100644 --- a/astrid/src/com/todoroo/astrid/ui/QuickAddBar.java +++ b/astrid/src/com/todoroo/astrid/ui/QuickAddBar.java @@ -60,7 +60,6 @@ import com.todoroo.astrid.repeats.RepeatControlSet; import com.todoroo.astrid.service.AddOnService; import com.todoroo.astrid.service.MetadataService; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.utility.Flags; import com.todoroo.astrid.voice.VoiceRecognizer; @@ -388,7 +387,6 @@ public class QuickAddBar extends LinearLayout { fragment.onTaskCreated(task); - StatisticsService.reportEvent(StatisticsConstants.TASK_CREATED_TASKLIST); return task; } catch (Exception e) { exceptionService.displayAndReportError(activity, diff --git a/astrid/src/com/todoroo/astrid/ui/RandomReminderControlSet.java b/astrid/src/com/todoroo/astrid/ui/RandomReminderControlSet.java index ccf61e264..22236b25a 100644 --- a/astrid/src/com/todoroo/astrid/ui/RandomReminderControlSet.java +++ b/astrid/src/com/todoroo/astrid/ui/RandomReminderControlSet.java @@ -18,7 +18,6 @@ import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.helper.TaskEditControlSet; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; /** * Control set dealing with random reminder settings @@ -101,7 +100,6 @@ public class RandomReminderControlSet extends TaskEditControlSet { int hourValue = hours[periodSpinner.getSelectedItemPosition()]; task.setValue(Task.REMINDER_PERIOD, hourValue * DateUtilities.ONE_HOUR); if (task.getSetValues().containsKey(Task.REMINDER_PERIOD.name)) { - StatisticsService.reportEvent(StatisticsConstants.RANDOM_REMINDER_SAVED); } } else { task.setValue(Task.REMINDER_PERIOD, 0L); diff --git a/astrid/src/com/todoroo/astrid/utility/Constants.java b/astrid/src/com/todoroo/astrid/utility/Constants.java index 5eca3be69..fd6e932f3 100644 --- a/astrid/src/com/todoroo/astrid/utility/Constants.java +++ b/astrid/src/com/todoroo/astrid/utility/Constants.java @@ -12,12 +12,6 @@ public final class Constants { // --- general application constants - /** - * LCL API Key - */ - public static final String LOCALYTICS_KEY_LITE = "f3a40b93823ac2024b062f2-d96a8860-4a2c-11e2-35ca-004b50a28849"; - public static final String LOCALYTICS_KEY = "ae35a010c66a997ab129ab7-3e2adf46-8bb3-11e0-fe8b-007f58cb3154"; - /** * Application Package */ @@ -78,10 +72,6 @@ public final class Constants { /** Notification Manager id for astrid.com */ public static final int NOTIFICATION_ACTFM = -5; - // --- crittercism - - public static final String CRITTERCISM_APP_ID = "4e8a796fddf5203b6f0097c5"; - // --- amazon public static final String AWS_ACCESS_KEY_ID = "AKIAJTVL4FOF4PRBKBNA"; diff --git a/astrid/src/com/todoroo/astrid/welcome/tutorial/WelcomeWalkthrough.java b/astrid/src/com/todoroo/astrid/welcome/tutorial/WelcomeWalkthrough.java index ad0a9aadf..2028d39d9 100644 --- a/astrid/src/com/todoroo/astrid/welcome/tutorial/WelcomeWalkthrough.java +++ b/astrid/src/com/todoroo/astrid/welcome/tutorial/WelcomeWalkthrough.java @@ -32,7 +32,6 @@ import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.actfm.ActFmGoogleAuthActivity; import com.todoroo.astrid.actfm.ActFmLoginActivity; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.viewpagerindicator.CirclePageIndicator; public class WelcomeWalkthrough extends ActFmLoginActivity { @@ -113,7 +112,6 @@ public class WelcomeWalkthrough extends ActFmLoginActivity { simpleLogin.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - StatisticsService.reportEvent(StatisticsConstants.ACTFM_LOGIN_SIMPLE); final ProgressDialog pd = DialogUtilities.progressDialog(WelcomeWalkthrough.this, getString(R.string.gtasks_GLA_authenticating)); pd.show(); getAuthToken(email, pd); @@ -175,7 +173,6 @@ public class WelcomeWalkthrough extends ActFmLoginActivity { rejectQuickLogin.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - StatisticsService.reportEvent(StatisticsConstants.ACTFM_LOGIN_SIMPLE_REJECTED); switchToLoginPage(); } }); diff --git a/astrid/src/com/todoroo/astrid/widget/WidgetConfigActivity.java b/astrid/src/com/todoroo/astrid/widget/WidgetConfigActivity.java index 0dfc75f10..c317841e8 100644 --- a/astrid/src/com/todoroo/astrid/widget/WidgetConfigActivity.java +++ b/astrid/src/com/todoroo/astrid/widget/WidgetConfigActivity.java @@ -23,7 +23,6 @@ import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.FilterWithCustomIntent; import com.todoroo.astrid.service.StatisticsConstants; -import com.todoroo.astrid.service.StatisticsService; import com.todoroo.astrid.service.ThemeService; @SuppressWarnings("nls") @@ -82,8 +81,6 @@ abstract public class WidgetConfigActivity extends ListActivity { Button button = (Button)findViewById(R.id.ok); button.setOnClickListener(mOnClickListener); - - StatisticsService.reportEvent(StatisticsConstants.WIDGET_CONFIG); } View.OnClickListener mOnClickListener = new View.OnClickListener() { @@ -114,14 +111,12 @@ abstract public class WidgetConfigActivity extends ListActivity { @Override protected void onResume() { super.onResume(); - StatisticsService.sessionStart(this); adapter.registerRecevier(); } @Override protected void onPause() { super.onPause(); - StatisticsService.sessionPause(); adapter.unregisterRecevier(); } @@ -133,7 +128,6 @@ abstract public class WidgetConfigActivity extends ListActivity { @Override protected void onStop() { super.onStop(); - StatisticsService.sessionStop(this); ThemeService.setForceFilterInvert(false); }