mirror of https://github.com/tasks/tasks
Updated localytics to 2.0
parent
ee4faa919e
commit
1f95ccf977
@ -0,0 +1,84 @@
|
|||||||
|
package com.localytics.android;
|
||||||
|
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build constants for the Localytics library.
|
||||||
|
* <p>
|
||||||
|
* 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 = "2.0"; //$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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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$
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,324 +1,353 @@
|
|||||||
|
//@formatter:off
|
||||||
/**
|
/**
|
||||||
* DatapointHelper.java
|
* DatapointHelper.java Copyright (C) 2011 Char Software Inc., DBA Localytics This code is provided under the Localytics Modified
|
||||||
* Copyright (C) 2009 Char Software Inc., DBA Localytics
|
* 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.
|
||||||
* 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;
|
package com.localytics.android;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import android.Manifest.permission;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.provider.Settings.System;
|
import android.net.NetworkInfo;
|
||||||
|
import android.os.Build;
|
||||||
import android.telephony.TelephonyManager;
|
import android.telephony.TelephonyManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a number of static functions to aid in the collection and formatting
|
* Provides a number of static functions to aid in the collection and formatting of datapoints.
|
||||||
* of datapoints.
|
* <p>
|
||||||
* @author Localytics
|
* Note: this is not a public API.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("nls")
|
/* package */final class DatapointHelper
|
||||||
public final class DatapointHelper
|
|
||||||
{
|
{
|
||||||
// This class should never be instantiated
|
/**
|
||||||
private DatapointHelper() { /**/ }
|
* AndroidID known to be duplicated across many devices due to manufacturer bugs.
|
||||||
|
|
||||||
private static final String LOG_PREFIX = "(DatapointHelper) ";
|
|
||||||
private static final String DROID2_ANDROID_ID = "9774d56d682e549c";
|
|
||||||
|
|
||||||
// Each YAML entry either goes to the session controller, the event controller
|
|
||||||
// or the optin controller
|
|
||||||
public static final String CONTROLLER_SESSION = "- c: se\n";
|
|
||||||
public static final String CONTROLLER_EVENT = "- c: ev\n";
|
|
||||||
public static final String CONTROLLER_OPT = "- c: optin\n";
|
|
||||||
|
|
||||||
// Each entry is either a create action, or an update action
|
|
||||||
public static final String ACTION_CREATE = " a: c\n";
|
|
||||||
public static final String ACTION_UPDATE = " a: u\n";
|
|
||||||
public static final String ACTION_OPTIN = " a: optin\n";
|
|
||||||
|
|
||||||
// The target object for the data being set up.
|
|
||||||
public static final String OBJECT_SESSION_DP = " se:\n";
|
|
||||||
public static final String OBJECT_EVENT_DP = " ev:\n";
|
|
||||||
public static final String OBJECT_OPT = " optin:\n";
|
|
||||||
|
|
||||||
// Events can have attributes
|
|
||||||
public static final String EVENT_ATTRIBUTE = " attrs:\n";
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
* WEBSERVICE PARAMENTER NAMES *
|
|
||||||
* *****************************
|
|
||||||
*/
|
*/
|
||||||
|
private static final String INVALID_ANDROID_ID = "9774d56d682e549c"; //$NON-NLS-1$
|
||||||
|
|
||||||
// Every object has a UUID
|
/**
|
||||||
public static final String PARAM_UUID = "u";
|
* The path to the device_id file in previous versions of the Localytics library
|
||||||
|
*/
|
||||||
// The identifier for this application, generated by the user on the webservice
|
private static final String LEGACY_DEVICE_ID_FILE = "/localytics/device_id"; //$NON-NLS-1$
|
||||||
public static final String PARAM_APP_UUID = "au";
|
|
||||||
|
|
||||||
// The version of this application, taken from the application's manifest.
|
|
||||||
public static final String PARAM_APP_VERSION = "av";
|
|
||||||
|
|
||||||
// A session's UUID as previously created.
|
|
||||||
public static final String PARAM_SESSION_UUID = "su";
|
|
||||||
|
|
||||||
// A hashed identifier unique to this device
|
|
||||||
public static final String PARAM_DEVICE_UUID = "du";
|
|
||||||
|
|
||||||
// android, iphone, blackberry, windowsmobile
|
|
||||||
public static final String PARAM_DEVICE_PLATFORM = "dp";
|
|
||||||
|
|
||||||
// maker of this device (currently not supported by Android)
|
|
||||||
public static final String PARAM_DEVICE_MAKE = "dma";
|
|
||||||
|
|
||||||
// model of the device
|
|
||||||
public static final String PARAM_DEVICE_MODEL = "dmo";
|
|
||||||
|
|
||||||
// version of the OS on this device
|
|
||||||
public static final String PARAM_OS_VERSION = "dov";
|
|
||||||
|
|
||||||
// country device is from (obtained by querying the SIM card)
|
|
||||||
public static final String PARAM_DEVICE_COUNTRY = "dc";
|
|
||||||
|
|
||||||
// country the current locale is set to
|
|
||||||
public static final String PARAM_LOCALE_COUNTRY = "dlc";
|
|
||||||
|
|
||||||
// country the language is set to
|
|
||||||
public static final String PARAM_LOCALE_LANGUAGE = "dll";
|
|
||||||
|
|
||||||
// Locale as a language_country string. (Not collected because this info
|
|
||||||
// is already provided by LOCALE_LANGUAGE and LOCALE_COUNTRY.
|
|
||||||
public static final String PARAM_LOCALE = "dl";
|
|
||||||
|
|
||||||
// Country the user is currently in (comes from Sim card)
|
|
||||||
public static final String PARAM_NETWORK_COUNTRY = "nc";
|
|
||||||
|
|
||||||
// Current carrier (comes from sim card)
|
/**
|
||||||
public static final String PARAM_NETWORK_CARRIER = "nca";
|
* 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$
|
||||||
|
}
|
||||||
|
|
||||||
// Current mobile network code (comes from sim card)
|
/**
|
||||||
public static final String PARAM_NETWORK_MNC = "mnc";
|
* 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (null != reader)
|
||||||
|
{
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final IOException e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// current mobile country code (comes from sim card)
|
final String androidId = android.provider.Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
|
||||||
public static final String PARAM_NETWORK_MCC = "mcc";
|
if (androidId == null || androidId.toLowerCase().equals(INVALID_ANDROID_ID))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// type of data connection (wifi, umts, gprs, evdo, ...)
|
return getSha256(androidId);
|
||||||
public static final String PARAM_DATA_CONNECTION = "dac";
|
}
|
||||||
|
|
||||||
// the version of this Localytics client library
|
/**
|
||||||
public static final String PARAM_LIBRARY_VERSION = "lv";
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("javadoc")
|
||||||
|
public static String getSerialNumberHashOrNull()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Obtain the device serial number using reflection, since serial number was added in SDK 9
|
||||||
|
*/
|
||||||
|
String serialNumber = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The source where the location came from
|
if (serialNumber == null)
|
||||||
public static final String PARAM_LOCATION_SOURCE = "ls";
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// the latitude returned by the location provider
|
return getSha256(serialNumber);
|
||||||
public static final String PARAM_LOCATION_LAT = "lat";
|
}
|
||||||
|
|
||||||
// the longitude from the location provider
|
/**
|
||||||
public static final String PARAM_LOCATION_LNG = "lng";
|
* Gets the device's telephony ID (e.g. IMEI/MEID).
|
||||||
|
* <p>
|
||||||
|
* 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 (Build.VERSION.SDK_INT >= 8)
|
||||||
|
{
|
||||||
|
final boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", new Class<?>[] { String.class }, new Object[] { "android.hardware.telephony" }); //$NON-NLS-1$//$NON-NLS-2$
|
||||||
|
|
||||||
// the current time on the user's device
|
if (!hasTelephony)
|
||||||
public static final String PARAM_CLIENT_TIME = "ct";
|
{
|
||||||
|
if (Constants.IS_LOGGABLE)
|
||||||
|
{
|
||||||
|
Log.i(Constants.LOG_TAG, "Device does not have telephony; cannot read telephony id"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
// sent at closing time, the current time on the users's device
|
return null;
|
||||||
public static final String PARAM_CLIENT_CLOSED_TIME = "ctc";
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The name an event that occured
|
/*
|
||||||
public static final String PARAM_EVENT_NAME = "n";
|
* 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$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// the optin value sent in if a user opts in or out.
|
return id;
|
||||||
public static final String PARAM_OPT_VALUE = "optin";
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the given key/value pair as a YAML string. This string is intended to be
|
* 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 define values for the first level of data in the YAML file. This is
|
* used to determine what device this data came from.
|
||||||
* different from the datapoints which belong another level in.
|
* <p>
|
||||||
* @param paramName The name of the parameter
|
* Note: this method will return null if this is a non-telephony device.
|
||||||
* @param paramValue The value of the parameter
|
* <p>
|
||||||
* @param paramIndent The indent level of the parameter
|
* Note: this method will return null if {@link permission#READ_PHONE_STATE} is not available.
|
||||||
* @return a YAML string which can be dumped to the YAML file
|
*
|
||||||
|
* @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 formatYAMLLine(String paramName,String paramValue, int paramIndent)
|
public static String getTelephonyDeviceIdHashOrNull(final Context context)
|
||||||
{
|
{
|
||||||
if (paramName.length() > LocalyticsSession.MAX_NAME_LENGTH)
|
if (Build.VERSION.SDK_INT >= 8)
|
||||||
{
|
{
|
||||||
Log.v(DatapointHelper.LOG_PREFIX, "Parameter name exceeds "
|
final boolean hasTelephony = ReflectionUtils.tryInvokeInstance(context.getPackageManager(), "hasSystemFeature", new Class<?>[] { String.class }, new Object[] { "android.hardware.telephony" }); //$NON-NLS-1$//$NON-NLS-2$
|
||||||
+ LocalyticsSession.MAX_NAME_LENGTH + " character limit. Truncating.");
|
|
||||||
paramName = paramName.substring(0, LocalyticsSession.MAX_NAME_LENGTH);
|
if (!hasTelephony)
|
||||||
|
{
|
||||||
|
if (Constants.IS_LOGGABLE)
|
||||||
|
{
|
||||||
|
Log.i(Constants.LOG_TAG, "Device does not have telephony; cannot read telephony id"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (paramValue.length() > LocalyticsSession.MAX_NAME_LENGTH)
|
|
||||||
|
/*
|
||||||
|
* Note: Sometimes Android will deny a package READ_PHONE_STATE permissions, even if the package has the permission. It
|
||||||
|
* appears to be a race condition that primarily occurs during installation.
|
||||||
|
*/
|
||||||
|
String id = null;
|
||||||
|
if (context.getPackageManager().checkPermission(permission.READ_PHONE_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED)
|
||||||
{
|
{
|
||||||
Log.v(DatapointHelper.LOG_PREFIX, "Parameter value exceeds "
|
final TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||||
+ LocalyticsSession.MAX_NAME_LENGTH + " character limit. Truncating.");
|
id = manager.getDeviceId();
|
||||||
paramValue = paramValue.substring(0, LocalyticsSession.MAX_NAME_LENGTH);
|
|
||||||
}
|
}
|
||||||
// The params are stored in the second tier of the YAML data.
|
else
|
||||||
// so with spacing, the expected result is: " paramname:paramvalue\n"
|
|
||||||
StringBuffer formattedString = new StringBuffer();
|
|
||||||
for (int currentIndent = 0; currentIndent < paramIndent; currentIndent++)
|
|
||||||
{
|
{
|
||||||
formattedString.append(" ");
|
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$
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedString.append(escapeString(paramName));
|
if (id == null)
|
||||||
formattedString.append(": ");
|
{
|
||||||
|
return null;
|
||||||
// Escape the string.
|
}
|
||||||
formattedString.append(escapeString(paramValue));
|
|
||||||
|
|
||||||
formattedString.append("\n");
|
|
||||||
|
|
||||||
return formattedString.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return getSha256(id);
|
||||||
* Gets a 1-way hashed value of the device's unique ID. This value is encoded using a SHA-256
|
|
||||||
* one way hash and cannot be used to determine what device this data came from.
|
|
||||||
* @param appContext The context used to access the settings resolver
|
|
||||||
* @return An 1-way hashed identifier unique to this device or null if an ID, or the hashing
|
|
||||||
* algorithm is not available.
|
|
||||||
*/
|
|
||||||
public static String getGlobalDeviceId(final Context appContext)
|
|
||||||
{
|
|
||||||
String systemId = System.getString(appContext.getContentResolver(), System.ANDROID_ID);
|
|
||||||
if(systemId == null || systemId.toLowerCase().equals(DROID2_ANDROID_ID))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] digest = md.digest(systemId.getBytes());
|
|
||||||
BigInteger hashedNumber = new BigInteger(1, digest);
|
|
||||||
return new String(hashedNumber.toString(16));
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(NoSuchAlgorithmException e)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the type of network this device is connected to.
|
* Determines the type of network this device is connected to.
|
||||||
* @param appContext the context used to access the device's WIFI
|
*
|
||||||
|
* @param context the context used to access the device's WIFI
|
||||||
* @param telephonyManager The manager used to access telephony info
|
* @param telephonyManager The manager used to access telephony info
|
||||||
* @return The type of network, or unknown if the information is unavailable
|
* @return The type of network, or unknown if the information is unavailable
|
||||||
*/
|
*/
|
||||||
public static String getNetworkType(
|
public static String getNetworkType(final Context context, final TelephonyManager telephonyManager)
|
||||||
final Context appContext,
|
|
||||||
TelephonyManager telephonyManager)
|
|
||||||
{
|
{
|
||||||
WifiManager wifiManager = (WifiManager)appContext.getSystemService(Context.WIFI_SERVICE);
|
if (context.getPackageManager().checkPermission(permission.ACCESS_WIFI_STATE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED)
|
||||||
|
|
||||||
// this will only work for apps which already have wifi permissions.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(wifiManager.isWifiEnabled())
|
|
||||||
{
|
|
||||||
return "wifi";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e) { /**/ }
|
|
||||||
|
|
||||||
switch (telephonyManager.getNetworkType())
|
|
||||||
{
|
{
|
||||||
case TelephonyManager.NETWORK_TYPE_EDGE : return "edge";
|
final NetworkInfo wifiInfo = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getNetworkInfo(ConnectivityManager.TYPE_WIFI);
|
||||||
case TelephonyManager.NETWORK_TYPE_GPRS : return "GPRS";
|
if (wifiInfo != null && wifiInfo.isConnectedOrConnecting())
|
||||||
case TelephonyManager.NETWORK_TYPE_UMTS : return "UMTS";
|
{
|
||||||
case TelephonyManager.NETWORK_TYPE_UNKNOWN : return "unknown";
|
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 "none";
|
return "android_network_type_" + telephonyManager.getNetworkType(); //$NON-NLS-1$
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the pretty string for this application's version.
|
|
||||||
* @param appContext The context used to examine packages
|
|
||||||
* @return The application's version as a pretty string
|
|
||||||
*/
|
|
||||||
public static String getAppVersion(final Context appContext)
|
|
||||||
{
|
|
||||||
PackageManager pm = appContext.getPackageManager();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return pm.getPackageInfo(appContext.getPackageName(), 0).versionName;
|
|
||||||
}
|
|
||||||
catch (PackageManager.NameNotFoundException e)
|
|
||||||
{
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current time, along with local timezone, formatted as a DateTime for the webservice.
|
* Gets the versionName of the application.
|
||||||
* @return a DateTime of the current local time and timezone.
|
*
|
||||||
|
* @param context {@link Context}. Cannot be null.
|
||||||
|
* @return The application's version
|
||||||
*/
|
*/
|
||||||
public static String getTimeAsDatetime()
|
public static String getAppVersion(final Context context)
|
||||||
{
|
{
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss-00:00");
|
final PackageManager pm = context.getPackageManager();
|
||||||
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
return sdf.format(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************
|
try
|
||||||
* Private Helper Functions *
|
{
|
||||||
***************************/
|
final String versionName = pm.getPackageInfo(context.getPackageName(), 0).versionName;
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Escapes strings for YAML parser
|
* If there is no versionName in the Android Manifest, the versionName will be null.
|
||||||
* @param rawString The string we want to escape.
|
*/
|
||||||
* @return An escaped string ready for YAML
|
if (versionName == null)
|
||||||
*/
|
{
|
||||||
private static String escapeString(String rawString)
|
if (Constants.IS_LOGGABLE)
|
||||||
{
|
{
|
||||||
StringBuffer parseString = new StringBuffer("\"");
|
Log.w(Constants.LOG_TAG, "versionName was null--is a versionName attribute set in the Android Manifest?"); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
int startRead = 0; // Index to start reading at
|
return "unknown"; //$NON-NLS-1$
|
||||||
int stopRead = 0; // Index characters get read from and where the substring ends
|
}
|
||||||
int bufferLength = rawString == null ? 0 : rawString.length();
|
|
||||||
|
|
||||||
if (rawString == null)
|
return versionName;
|
||||||
|
}
|
||||||
|
catch (final PackageManager.NameNotFoundException e)
|
||||||
{
|
{
|
||||||
return "";
|
/*
|
||||||
|
* This should never occur--our own package must exist for this code to be running
|
||||||
|
*/
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Every time we come across a " or \, append what we have so far, append a \,
|
/**
|
||||||
// then manage our indexes to continue where we left off.
|
* Helper method to generate a SHA-256 hash of a given String
|
||||||
while (stopRead < bufferLength)
|
*
|
||||||
|
* @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 (rawString.charAt(stopRead) == '\"' || rawString.charAt(stopRead) == '\\')
|
if (null == string)
|
||||||
{
|
|
||||||
parseString.append(rawString.substring(startRead, stopRead));
|
|
||||||
parseString.append('\\');
|
|
||||||
startRead = stopRead;
|
|
||||||
}
|
|
||||||
// Skip null characters.
|
|
||||||
else if (rawString.charAt(stopRead) == '\0')
|
|
||||||
{
|
{
|
||||||
parseString.append(rawString.substring(startRead, stopRead));
|
throw new IllegalArgumentException("string cannot be null"); //$NON-NLS-1$
|
||||||
startRead = stopRead + 1;
|
|
||||||
}
|
}
|
||||||
stopRead++;
|
|
||||||
}
|
}
|
||||||
// Append whatever is left after parsing
|
|
||||||
parseString.append(rawString.substring(startRead, stopRead));
|
try
|
||||||
// and finish with a closing "
|
{
|
||||||
parseString.append('\"');
|
final MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$
|
||||||
return parseString.toString();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.localytics.android;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception handler for background threads used by the Localytics library.
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
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$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,569 @@
|
|||||||
|
package com.localytics.android;
|
||||||
|
|
||||||
|
import android.Manifest.permission;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Data connection type.
|
||||||
|
*/
|
||||||
|
public static final String KEY_DATA_CONNECTION = "dac"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
*/
|
||||||
|
public static final String KEY_DEVICE_COUNTRY = "dc"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Model of the device (e.g. dream,
|
||||||
|
*/
|
||||||
|
public static final String KEY_DEVICE_MODEL = "dmo"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* SDK compatibility level of the device.
|
||||||
|
*
|
||||||
|
* @see android.os.Build.VERSION#SDK_INT
|
||||||
|
*/
|
||||||
|
public static final String KEY_DEVICE_SDK_LEVEL = "dsdk"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Country for the device's current locale settings
|
||||||
|
*/
|
||||||
|
public static final String KEY_LOCALE_COUNTRY = "dlc"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* Language for the device's current locale settings
|
||||||
|
*/
|
||||||
|
public static final String KEY_LOCALE_LANGUAGE = "dll"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* Api key
|
||||||
|
*/
|
||||||
|
public static final String KEY_LOCALYTICS_API_KEY = "au"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Network carrier of the device
|
||||||
|
*/
|
||||||
|
public static final String KEY_NETWORK_CARRIER = "nca"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
*/
|
||||||
|
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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Epoch timestamp when the session was started in seconds.
|
||||||
|
*/
|
||||||
|
public static final String KEY_WALL_TIME_SECONDS = "ct"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code long}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Data type for the JSON object.
|
||||||
|
*
|
||||||
|
* @see #VALUE_DATA_TYPE
|
||||||
|
*/
|
||||||
|
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* UUID of the event.
|
||||||
|
*/
|
||||||
|
public static final String KEY_EVENT_UUID = "u"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String[]} (technically, a JSON array of strings)
|
||||||
|
* <p>
|
||||||
|
* Ordered list of flow events that occurred
|
||||||
|
*/
|
||||||
|
public static final String KEY_FLOW_ARRAY = "fl"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code long}
|
||||||
|
* <p>
|
||||||
|
* Epoch timestamp when the session was started
|
||||||
|
*/
|
||||||
|
public static final String KEY_SESSION_LENGTH_SECONDS = "ctl"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code long}
|
||||||
|
* <p>
|
||||||
|
* Start time of the parent session
|
||||||
|
*/
|
||||||
|
public static final String KEY_SESSION_START_TIME = "ss"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* UUID of the session.
|
||||||
|
*/
|
||||||
|
public static final String KEY_SESSION_UUID = "su"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code long}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Epoch timestamp when the session was started in seconds.
|
||||||
|
*/
|
||||||
|
public static final String KEY_WALL_TIME_SECONDS = "ct"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* UUID of the session.
|
||||||
|
*/
|
||||||
|
public static final String KEY_SESSION_UUID = "su"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* UUID of the event.
|
||||||
|
*/
|
||||||
|
public static final String KEY_EVENT_UUID = "u"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* Name of the event.
|
||||||
|
*/
|
||||||
|
public static final String KEY_NAME = "n"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code JSONObject}.
|
||||||
|
* <p>
|
||||||
|
* Maps to the attributes of the event.
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Epoch timestamp when the session was started in seconds.
|
||||||
|
*/
|
||||||
|
public static final String KEY_WALL_TIME_SECONDS = "ct"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code String}
|
||||||
|
* <p>
|
||||||
|
* API key
|
||||||
|
*/
|
||||||
|
public static final String KEY_API_KEY = "u"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code boolean}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* Data type for the JSON object.
|
||||||
|
*
|
||||||
|
* @see #VALUE_DATA_TYPE
|
||||||
|
*/
|
||||||
|
public static final String KEY_DATA_TYPE = "dt"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type: {@code long}
|
||||||
|
* <p>
|
||||||
|
* 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}
|
||||||
|
* <p>
|
||||||
|
* 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)
|
||||||
|
* <p>
|
||||||
|
* 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)
|
||||||
|
* <p>
|
||||||
|
* 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$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,112 @@
|
|||||||
|
package com.localytics.android;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static utilities for performing reflection against newer Android SDKs.
|
||||||
|
* <p>
|
||||||
|
* 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 <T> 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
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T tryInvokeStatic(final Class<?> classObject, final String methodName, final Class<?>[] types, final Object[] args)
|
||||||
|
{
|
||||||
|
return (T) helper(null, classObject, null, methodName, types, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use reflection to invoke a static method for a class object and method name
|
||||||
|
*
|
||||||
|
* @param <T> 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
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T tryInvokeStatic(final String className, final String methodName, final Class<?>[] types, final Object[] args)
|
||||||
|
{
|
||||||
|
return (T) helper(className, null, null, methodName, types, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use reflection to invoke a static method for a class object and method name
|
||||||
|
*
|
||||||
|
* @param <T> 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
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T tryInvokeInstance(final Object target, final String methodName, final Class<?>[] types, final Object[] args)
|
||||||
|
{
|
||||||
|
return (T) helper(target, null, null, methodName, types, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,353 +0,0 @@
|
|||||||
/**
|
|
||||||
* UploaderThread.java
|
|
||||||
* Copyright (C) 2009 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.localytics.android;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FilenameFilter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
|
|
||||||
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.StringEntity;
|
|
||||||
import org.apache.http.impl.client.DefaultHttpClient;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The thread which handles uploading Localytics data.
|
|
||||||
* @author Localytics
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("nls")
|
|
||||||
public class UploaderThread extends Thread
|
|
||||||
{
|
|
||||||
private final Runnable _completeCallback;
|
|
||||||
private final File _localyticsDir;
|
|
||||||
private final String _sessionFilePrefix;
|
|
||||||
private final String _uploaderFilePrefix;
|
|
||||||
private final String _closeFilePrefix;
|
|
||||||
|
|
||||||
// The Tag used in logging.
|
|
||||||
private final static String LOG_TAG = "Localytics_uploader";
|
|
||||||
|
|
||||||
// The URL to send Localytics session data to
|
|
||||||
private final static String ANALYTICS_URL = "http://analytics.localytics.com/api/datapoints/bulk";
|
|
||||||
|
|
||||||
// The size of the buffer used for reading in files.
|
|
||||||
private final static int BUFFER_SIZE = 1024;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a thread which uploads the session files in the passed Localytics
|
|
||||||
* Directory. All files starting with sessionFilePrefix are renamed,
|
|
||||||
* uploaded and deleted on upload. This way the sessions can continue
|
|
||||||
* writing data regardless of whether or not the upload succeeds. Files
|
|
||||||
* which have been renamed still count towards the total number of Localytics
|
|
||||||
* files which can be stored on the disk.
|
|
||||||
* @param appContext The context used to access the disk
|
|
||||||
* @param completeCallback A runnable which is called notifying the caller that upload is complete.
|
|
||||||
* @param localyticsDir The directory containing the session files
|
|
||||||
* @param sessionFilePrefix The filename prefix identifying the session files.
|
|
||||||
* @param uploaderfilePrefix The filename prefixed identifying files to be uploaded.
|
|
||||||
*/
|
|
||||||
public UploaderThread(
|
|
||||||
File localyticsDir,
|
|
||||||
String sessionFilePrefix,
|
|
||||||
String uploaderFilePrefix,
|
|
||||||
String closeFilePrefix,
|
|
||||||
Runnable completeCallback)
|
|
||||||
{
|
|
||||||
this._localyticsDir = localyticsDir;
|
|
||||||
this._sessionFilePrefix = sessionFilePrefix;
|
|
||||||
this._uploaderFilePrefix = uploaderFilePrefix;
|
|
||||||
this._closeFilePrefix = closeFilePrefix;
|
|
||||||
this._completeCallback = completeCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renames all the session files (so that other threads can keep writing
|
|
||||||
* datapoints without affecting the upload. And then uploads them.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
int numFilesToUpload = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(this._localyticsDir != null && this._localyticsDir.exists())
|
|
||||||
{
|
|
||||||
String basePath = this._localyticsDir.getAbsolutePath();
|
|
||||||
|
|
||||||
// rename all the files, taking care to rename the session files
|
|
||||||
// before the close files.
|
|
||||||
renameOrAppendSessionFiles(basePath);
|
|
||||||
renameOrAppendCloseFiles(basePath);
|
|
||||||
|
|
||||||
// Grab all the files to be uploaded
|
|
||||||
FilenameFilter filter = new FilenameFilter()
|
|
||||||
{
|
|
||||||
public boolean accept(File dir, String name)
|
|
||||||
{
|
|
||||||
return name.startsWith(_uploaderFilePrefix);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
String uploaderFiles[] = this._localyticsDir.list(filter);
|
|
||||||
numFilesToUpload = uploaderFiles.length;
|
|
||||||
String postBody = createPostBodyFromFiles(basePath, uploaderFiles);
|
|
||||||
|
|
||||||
// Attempt to upload this data. If successful, delete all the uploaderFiles.
|
|
||||||
Log.v(UploaderThread.LOG_TAG, "Attempting to upload " + numFilesToUpload + " files.");
|
|
||||||
if(uploadSessions(postBody.toString()) == true)
|
|
||||||
{
|
|
||||||
int currentFile;
|
|
||||||
File uploadedFile;
|
|
||||||
for(currentFile = 0; currentFile < uploaderFiles.length; currentFile++)
|
|
||||||
{
|
|
||||||
uploadedFile = new File(basePath + "/" + uploaderFiles[currentFile]);
|
|
||||||
uploadedFile.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify the caller the upload is complete.
|
|
||||||
if(this._completeCallback != null)
|
|
||||||
{
|
|
||||||
this._completeCallback.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Log.v(UploaderThread.LOG_TAG, "Swallowing exception: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks at every file whose name starts with the session file prefix
|
|
||||||
* and renamed or appends it to the appropriately named uploader file.
|
|
||||||
* @param basePath The full path to the directory containing the files to upload
|
|
||||||
*/
|
|
||||||
private void renameOrAppendSessionFiles(String basePath)
|
|
||||||
{
|
|
||||||
int currentFile;
|
|
||||||
|
|
||||||
// Create a filter to only grab the session files.
|
|
||||||
FilenameFilter filter = new FilenameFilter()
|
|
||||||
{
|
|
||||||
public boolean accept(File dir, String name)
|
|
||||||
{
|
|
||||||
return name.startsWith(_sessionFilePrefix);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Go through each of the session files
|
|
||||||
String[] originalFiles = this._localyticsDir.list(filter);
|
|
||||||
for(currentFile = 0; currentFile < originalFiles.length; currentFile++)
|
|
||||||
{
|
|
||||||
String originalFileName = basePath + "/" + originalFiles[currentFile];
|
|
||||||
String targetFileName = basePath + "/" + this._uploaderFilePrefix + originalFiles[currentFile];
|
|
||||||
renameOrAppendFile(new File(originalFileName), new File(targetFileName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks at every close file in the directory and renames or appends it to
|
|
||||||
* the appropriate uploader file. This is done separately from the session
|
|
||||||
* files because it makes life simpler on the webservice if the close events
|
|
||||||
* come after the session events
|
|
||||||
* @param basePath The full path to the directory containing the files to upload
|
|
||||||
*/
|
|
||||||
private void renameOrAppendCloseFiles(String basePath)
|
|
||||||
{
|
|
||||||
int currentFile;
|
|
||||||
|
|
||||||
// Create a filter to only grab the session files.
|
|
||||||
FilenameFilter filter = new FilenameFilter()
|
|
||||||
{
|
|
||||||
public boolean accept(File dir, String name)
|
|
||||||
{
|
|
||||||
return name.startsWith(_closeFilePrefix);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Go through each of the session files
|
|
||||||
String[] originalFiles = this._localyticsDir.list(filter);
|
|
||||||
for(currentFile = 0; currentFile < originalFiles.length; currentFile++)
|
|
||||||
{
|
|
||||||
String originalFileName = basePath + "/" + originalFiles[currentFile];
|
|
||||||
|
|
||||||
// In order for the close events to be appended to the appropriate files
|
|
||||||
// remove the close prefix and prepend the session prefix
|
|
||||||
String targetFileName = basePath + "/"
|
|
||||||
+ this._uploaderFilePrefix
|
|
||||||
+ getSessionFilenameFromCloseFile(originalFiles[currentFile]);
|
|
||||||
renameOrAppendFile(new File(originalFileName), new File(targetFileName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines what the name of the session file matching this close file would be
|
|
||||||
* @param closeFilename Name of close file to be used as a guide
|
|
||||||
* @return The filename of the session which matches this close file
|
|
||||||
*/
|
|
||||||
private String getSessionFilenameFromCloseFile(String closeFilename)
|
|
||||||
{
|
|
||||||
return this._sessionFilePrefix + closeFilename.substring(this._closeFilePrefix.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if destination file exists. If so, it appends the contents of
|
|
||||||
* source to destination and deletes source. Otherwise, it rename source
|
|
||||||
* to destination.
|
|
||||||
* @param source File containing the data to be moved
|
|
||||||
* @param destination Target for the data
|
|
||||||
*/
|
|
||||||
private static void renameOrAppendFile(File source, File destination)
|
|
||||||
{
|
|
||||||
if(destination.exists())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InputStream in = new FileInputStream(source);
|
|
||||||
OutputStream out = new FileOutputStream(destination, true);
|
|
||||||
|
|
||||||
byte[] buf = new byte[1024];
|
|
||||||
int len;
|
|
||||||
while ((len = in.read(buf)) > 0)
|
|
||||||
{
|
|
||||||
out.write(buf, 0, len);
|
|
||||||
}
|
|
||||||
in.close();
|
|
||||||
out.close();
|
|
||||||
source.delete();
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException e)
|
|
||||||
{
|
|
||||||
Log.v(LOG_TAG, "File not found.");
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
Log.v(LOG_TAG, "IO Exception: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
source.renameTo(destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads in the input files and cats them together in one big string which makes up the
|
|
||||||
* HTTP request body.
|
|
||||||
* @param basePath The directory to get the files from
|
|
||||||
* @param uploaderFiles the list of files to read
|
|
||||||
* @return A string containing a YML blob which can be uploaded to the webservice.
|
|
||||||
*/
|
|
||||||
private String createPostBodyFromFiles(final String basePath, final String[] uploaderFiles)
|
|
||||||
{
|
|
||||||
int currentFile;
|
|
||||||
File inputFile;
|
|
||||||
StringBuffer postBody = new StringBuffer();
|
|
||||||
|
|
||||||
// Read each file in to one buffer. This allows the upload to happen as one
|
|
||||||
// large transfer instead of many smaller transfers which is preferable on
|
|
||||||
// a mobile device in which the time required to make a connection is often
|
|
||||||
// disproportionately large compared to the time to upload the data.
|
|
||||||
for(currentFile = 0; currentFile < uploaderFiles.length; currentFile++)
|
|
||||||
{
|
|
||||||
inputFile = new File(basePath + "/" + uploaderFiles[currentFile]);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
BufferedReader reader = new BufferedReader(
|
|
||||||
new InputStreamReader(
|
|
||||||
new FileInputStream(inputFile),
|
|
||||||
"UTF-8"),
|
|
||||||
UploaderThread.BUFFER_SIZE);
|
|
||||||
char[] buf = new char[1024];
|
|
||||||
int numRead;
|
|
||||||
while( (numRead = reader.read(buf)) > 0)
|
|
||||||
{
|
|
||||||
postBody.append(buf, 0, numRead);
|
|
||||||
}
|
|
||||||
reader.close();
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException e)
|
|
||||||
{
|
|
||||||
Log.v(LOG_TAG, "File Not Found");
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
Log.v(LOG_TAG, "IOException: " + e.getMessage());
|
|
||||||
}
|
|
||||||
catch (OutOfMemoryError e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
Log.v(LOG_TAG, "OutOfMemoryError: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return postBody.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads the post Body to the webservice
|
|
||||||
* @param ymlBlob String containing the YML to upload
|
|
||||||
* @return True on success, false on failure.
|
|
||||||
*/
|
|
||||||
private boolean uploadSessions(String ymlBlob)
|
|
||||||
{
|
|
||||||
DefaultHttpClient client = new DefaultHttpClient();
|
|
||||||
HttpPost method = new HttpPost(UploaderThread.ANALYTICS_URL);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
StringEntity postBody = new StringEntity(ymlBlob, "utf8");
|
|
||||||
method.setEntity(postBody);
|
|
||||||
HttpResponse response = client.execute(method);
|
|
||||||
|
|
||||||
StatusLine status = response.getStatusLine();
|
|
||||||
Log.v(UploaderThread.LOG_TAG, "Upload complete. Status: " + status.getStatusCode());
|
|
||||||
|
|
||||||
// On any response from the webservice, return true so the local files get
|
|
||||||
// deleted. This avoid an infinite loop in which a bad file keeps getting
|
|
||||||
// submitted to the webservice time and again.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// return true for any transportation errors.
|
|
||||||
catch (UnsupportedEncodingException e)
|
|
||||||
{
|
|
||||||
Log.v(LOG_TAG, "UnsuppEncodingException: " + e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (ClientProtocolException e)
|
|
||||||
{
|
|
||||||
Log.v(LOG_TAG, "ClientProtocolException: " + e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
Log.v(LOG_TAG, "IOException: " + e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue