using System;
using System.Collections.Generic;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.IsolatedStorage;
using System.Text;
public class LocalyticsSession
{
#region library constants
private const int maxStoredSessions = 10;
private const int maxNameLength = 100;
private const string directoryName = "localytics";
private const string sessionFilePrefix = "s_";
private const string uploadFilePrefix = "u_";
private const string closeFilePrefix = "c_";
private const string serviceURL = "http://analytics.localytics.com/api/datapoints/bulk";
#endregion
#region webservice constants
private const string CONTROLLER_SESSION = "- c: se\n";
private const string CONTROLLER_EVENT = "- c: ev\n";
private const string CONTROLLER_OPT = "- c: optin\n";
private const string ACTION_CREATE = " a: c\n";
private const string ACTION_UPDATE = " a: u\n";
private const string ACTION_OPTIN = " a: optin\n";
private const string OBJECT_SESSION_DP = " se:\n";
private const string OBJECT_EVENT_DP = " ev:\n";
private const string OBJECT_OPT = " optin:\n";
private const string EVENT_ATTRIBUTE = " attrs:\n";
private const string PARAM_UUID = "u"; // session ID
private const string PARAM_APP_UUID = "au"; // app id
private const string PARAM_APP_VERSION = "av"; // app version
private const string PARAM_SESSION_UUID = "su"; // the session id (previously created)
private const string PARAM_DEVICE_UUID = "du"; // user id
private const string PARAM_DEVICE_PLATFORM = "dp"; // platform
private const string PARAM_DEVICE_MAKE = "dma"; // make
private const string PARAM_DEVICE_MODEL = "dmo"; // model
private const string PARAM_OS_VERSION = "dov"; // os version
private const string PARAM_DEVICE_COUNTRY = "dc"; // device country
private const string PARAM_LOCALE_COUNTRY = "dlc"; // country device is set to
private const string PARAM_LOCALE_LANGUAGE = "dll"; // language the device is set to
private const string PARAM_LOCALE = "dl"; // locale
private const string PARAM_NETWORK_COUNTRY = "nc"; // counry from network
private const string PARAM_NETWORK_CARRIER = "nca"; // carrier
private const string PARAM_NETWORK_MNC = "mnc"; // mnc
private const string PARAM_NETWORK_MCC = "mcc"; // mcc
private const string PARAM_DATA_CONNECTION = "dac"; // type of data connection available
private const string PARAM_LIBRARY_VERSION = "lv"; // version of the client library
private const string PARAM_LOCATION_SOURCE = "ls"; // where the location comes from if it's present
private const string PARAM_LOCATION_LAT = "lat"; // lat
private const string PARAM_LOCATION_LNG = "lng"; // lng
private const string PARAM_CLIENT_TIME = "ct"; // time from client's phone
private const string PARAM_CLIENT_CLOSED_TIME = "ctc"; // time the session is closed at
private const string PARAM_EVENT_NAME = "n"; // name of an event
private const string PARAM_OPT_VALUE = "optin"; // value of user's opt in/out decision
private const string CLIENT_VERSION = "windowsphone.1.0";
#endregion
#region private members
private string appKey;
private string sessionUuid;
private string sessionFilename;
private bool isSessionOpen = false;
private bool isSessionClosed = false;
private double latitude = 0;
private double longitude = 0;
#endregion
#region static members
private static bool isUploading = false;
private static IsolatedStorageFile localStorage = null;
#endregion
#region private methods
#region Storage
///
/// Caches the reference to the app's isolated storage
///
private static IsolatedStorageFile getStore()
{
if (localStorage == null)
{
localStorage = IsolatedStorageFile.GetUserStoreForApplication();
}
return localStorage;
}
///
/// Tallies up the number of files whose name starts w/ sessionFilePrefix in the localytics dir
///
private static int getNumberOfStoredSessions()
{
IsolatedStorageFile store = getStore();
if (store.DirectoryExists(LocalyticsSession.directoryName) == false)
{
return 0;
}
return store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.sessionFilePrefix + "*").Length;
}
///
/// Gets a stream pointing to the requested file. If the file does not exist it is created.
/// If the file does exist the stream points to the end of the file
///
/// Name of the file (w/o directory) to create
private static IsolatedStorageFileStream getStreamForFile(string filename)
{
IsolatedStorageFile store = getStore();
store.CreateDirectory(LocalyticsSession.directoryName); // does nothing if dir exists
return new IsolatedStorageFileStream(LocalyticsSession.directoryName + @"\" + filename, FileMode.Append, store);
}
///
/// Appends a string to the end of a text file
///
/// Text to append
/// Name of file to append to
private static void appendTextToFile(string text, string filename)
{
IsolatedStorageFileStream file = getStreamForFile(filename);
TextWriter writer = new StreamWriter(file);
writer.Write(text);
writer.Close();
file.Close();
}
///
/// Reads a file and returns its contents as a string
///
/// file to read (w/o directory prefix)
/// the contents of the file
private static string GetFileContents(string filename)
{
IsolatedStorageFile store = getStore();
var file = store.OpenFile(LocalyticsSession.directoryName + @"\" + filename, FileMode.Open);
TextReader reader = new StreamReader(file);
string contents = reader.ReadToEnd();
reader.Close();
file.Close();
return contents;
}
#endregion
#region upload
///
/// Goes through all the upload files and collects their contents for upload
///
/// A string containing the concatenated
private static string GetUploadContents()
{
StringBuilder contents = new StringBuilder();
IsolatedStorageFile store = getStore();
if (store.DirectoryExists(LocalyticsSession.directoryName))
{
string[] files = store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.uploadFilePrefix + "*");
foreach (string file in files)
{
if (file.StartsWith(LocalyticsSession.uploadFilePrefix)) // workaround forGetFileNames bug
{
contents.Append(GetFileContents(file));
}
}
}
return contents.ToString();
}
///
/// loops through all the files in the directory deleting the upload files
///
private static void DeleteUploadFiles()
{
IsolatedStorageFile store = getStore();
if (store.DirectoryExists(LocalyticsSession.directoryName))
{
string[] files = store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.uploadFilePrefix + "*");
foreach (string file in files)
{
if (file.StartsWith(LocalyticsSession.uploadFilePrefix)) // workaround for GetfileNames returning extra files
{
store.DeleteFile(LocalyticsSession.directoryName + @"\" + file);
}
}
}
}
///
/// Rename any open session files. This way events recorded during uploaded get written safely to disk
/// and threading difficulties are missed.
///
private static void renameOrAppendSessionFiles()
{
IsolatedStorageFile store = getStore();
if (store.DirectoryExists(LocalyticsSession.directoryName))
{
string[] files = store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.sessionFilePrefix + "*");
foreach (string file in files)
{
if (file.StartsWith(LocalyticsSession.sessionFilePrefix)) // work around for GetFileNames returning extra files
{
string destinationFilename = LocalyticsSession.uploadFilePrefix + file;
appendTextToFile(GetFileContents(file), destinationFilename);
store.DeleteFile(LocalyticsSession.directoryName + @"\" + file);
}
}
}
}
///
/// Runs on a seperate thread and is responsible for renaming and uploading files as appropriate
///
private static void BeginUpload()
{
logMessage("Beginning upload.");
try
{
renameOrAppendSessionFiles();
// begin the upload
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(LocalyticsSession.serviceURL);
myRequest.Method = "POST";
myRequest.ContentType = "application/x-yaml";
myRequest.BeginGetRequestStream(new AsyncCallback(httpRequestCallback), myRequest);
}
catch (Exception e)
{
logMessage("Swallowing exception: " + e.Message);
}
}
private static void httpRequestCallback(IAsyncResult asynchronousResult)
{
try
{
HttpWebRequest request = (HttpWebRequest)asynchronousResult.AsyncState;
Stream postStream = request.EndGetRequestStream(asynchronousResult);
byte[] byteArray = Encoding.UTF8.GetBytes(GetUploadContents());
postStream.Write(byteArray, 0, byteArray.Length);
postStream.Close();
request.BeginGetResponse(new AsyncCallback(GetResponseCallback), request);
}
catch (Exception e)
{
logMessage("Swallowing exception: " + e.Message);
}
}
private static void GetResponseCallback(IAsyncResult asynchronousResult)
{
try
{
HttpWebRequest request = (HttpWebRequest)asynchronousResult.AsyncState;
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asynchronousResult);
Stream streamResponse = response.GetResponseStream();
StreamReader streamRead = new StreamReader(streamResponse);
string responsestring = streamRead.ReadToEnd();
logMessage("Upload complete. Response: " + responsestring);
DeleteUploadFiles();
streamResponse.Close();
streamRead.Close();
response.Close();
}
catch (WebException e)
{
Debug.WriteLine("WebException raised.");
Debug.WriteLine("\n{0}", e.Message);
Debug.WriteLine("\n{0}", e.Status);
}
catch (Exception e)
{
Debug.WriteLine("Exception raised!");
Debug.WriteLine("Message : " + e.Message);
}
finally
{
LocalyticsSession.isUploading = false;
}
}
#endregion
///
/// Retreives a unique identifier for this device. According to Microsoft, this identifier is
/// unique across all carriers and devices
///
private static string getDeviceId()
{
byte[] id = (byte[])Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceUniqueId");
return Convert.ToBase64String(id);
}
///
/// Gets the current date/time as a string which can be inserted in the DB
///
/// A formatted string with date, time, and timezone information
private static string GetDatestring()
{
DateTime dt = DateTime.Now.ToUniversalTime();
// reformat the time to: YYYY-MM-DDTHH:MM:SS
// use a StringBuilder to avoid creating multiple
StringBuilder datestring = new StringBuilder();
datestring.Append(dt.Year);
datestring.Append("-");
datestring.Append(dt.Month.ToString("D2"));
datestring.Append("-");
datestring.Append(dt.Day.ToString("D2"));
datestring.Append("T");
datestring.Append(dt.Hour.ToString("D2"));
datestring.Append(":");
datestring.Append(dt.Minute.ToString("D2"));
datestring.Append(":");
datestring.Append(dt.Second.ToString("D2"));
return datestring.ToString();
}
///
/// Gathers all the parameters associated w/ a session open
///
/// A YAML formatted string to be saved w/ the session open
private string getOpenSessionData()
{
StringBuilder openstring = new StringBuilder();
string appVersion = System.Reflection.Assembly.GetExecutingAssembly().FullName.Split('=')[1].Split(',')[0];
openstring.Append(LocalyticsSession.CONTROLLER_SESSION);
openstring.Append(LocalyticsSession.ACTION_CREATE);
openstring.Append(LocalyticsSession.OBJECT_SESSION_DP);
// Application and session information
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_UUID, this.sessionUuid, 3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_APP_UUID, this.appKey, 3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_APP_VERSION, appVersion, 3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_LIBRARY_VERSION, LocalyticsSession.CLIENT_VERSION, 3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_CLIENT_TIME, GetDatestring(), 3));
// Other device information
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_DEVICE_UUID, getDeviceId(), 3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_DEVICE_PLATFORM, "Windows Phone", 3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_OS_VERSION, Environment.OSVersion.Version.Build.ToString(), 3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_DEVICE_MODEL,
Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceName").ToString(),
3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_DEVICE_MAKE,
Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceManufacturer").ToString(),
3));
openstring.Append(formatYAMLLine(LocalyticsSession.PARAM_LOCALE_LANGUAGE,
CultureInfo.CurrentCulture.TwoLetterISOLanguageName,
3));
return openstring.ToString();
}
///
/// Formats an input string for YAML
///
/// string sorrounded in quotes, with dangerous characters escaped
private static string escapestring(string input)
{
string escapedSlahes = input.Replace("\\", "\\\\");
return "\"" + escapedSlahes.Replace("\"", "\\\"") + "\"";
}
///
/// Returns the given key/value pair as a YAML line.
///
/// The name of the parameter
/// The value for the parameter
/// How many spaces from the top level to indent this attribute
///
private static string formatYAMLLine(string paramName, string paramValue, int paramIndent)
{
if (paramName.Length > LocalyticsSession.maxNameLength)
{
logMessage("Parameter name: " + paramName + " exceeds max name length. Truncating.");
paramName = paramName.Substring(0, LocalyticsSession.maxNameLength);
}
if (paramValue.Length > LocalyticsSession.maxNameLength)
{
logMessage("Parameter name: " + paramValue + " exceeds max name length. Truncating.");
paramValue = paramValue.Substring(0, LocalyticsSession.maxNameLength);
}
StringBuilder formattedstring = new StringBuilder();
for (int i = 0; i < paramIndent; i++)
{
formattedstring.Append(" ");
}
formattedstring.Append(escapestring(paramName));
formattedstring.Append(": ");
formattedstring.Append(escapestring(paramValue));
formattedstring.Append(Environment.NewLine);
return formattedstring.ToString();
}
///
/// Outputs a message to the debug console
///
private static void logMessage(string msg)
{
Debug.WriteLine("(localytics) " + msg);
}
#endregion
#region public methods
///
/// Creates a Localytics Session object
///
/// The key unique for each application generated at www.localytics.com
public LocalyticsSession(string appKey)
{
this.appKey = appKey;
}
///
/// Opens or resumes the Localytics session.
///
public void open()
{
if (this.isSessionOpen || this.isSessionClosed)
{
logMessage("Session is already opened or closed.");
return;
}
try
{
if (getNumberOfStoredSessions() > LocalyticsSession.maxStoredSessions)
{
logMessage("Local stored session count exceeded.");
return;
}
this.sessionUuid = Guid.NewGuid().ToString();
this.sessionFilename = LocalyticsSession.sessionFilePrefix + this.sessionUuid;
appendTextToFile(getOpenSessionData(), this.sessionFilename);
this.isSessionOpen = true;
logMessage("Session opened.");
}
catch (Exception e)
{
logMessage("Swallowing exception: " + e.Message);
}
}
///
/// Closes the Localytics session.
///
public void close()
{
if (this.isSessionOpen == false || this.isSessionClosed == true)
{
logMessage("Session not closed b/c it is either not open or already closed.");
return;
}
try
{
StringBuilder closeString = new StringBuilder();
closeString.Append(LocalyticsSession.CONTROLLER_SESSION);
closeString.Append(LocalyticsSession.ACTION_UPDATE);
closeString.Append(formatYAMLLine(LocalyticsSession.PARAM_UUID, this.sessionUuid, 2));
closeString.Append(LocalyticsSession.OBJECT_SESSION_DP);
closeString.Append(formatYAMLLine(LocalyticsSession.PARAM_APP_UUID, this.appKey, 3));
closeString.Append(formatYAMLLine(LocalyticsSession.PARAM_CLIENT_CLOSED_TIME, GetDatestring(), 3));
if (latitude != 0 && longitude != 0)
{
closeString.Append(formatYAMLLine("lat", latitude.ToString(), 3));
closeString.Append(formatYAMLLine("lng", longitude.ToString(), 3));
}
appendTextToFile(closeString.ToString(), this.sessionFilename);
logMessage("Session closed.");
}
catch (Exception e)
{
logMessage("Swallowing exception: " + e.Message);
}
}
///
/// Creates a new thread which collects any files and uploads them. Returns immediately if an upload
/// is already happenning.
///
public void upload()
{
if (isUploading)
{
return;
}
isUploading = true;
try
{
// Do all the upload work on a seperate thread.
System.Threading.ThreadStart uploadJob = new System.Threading.ThreadStart(BeginUpload);
System.Threading.Thread uploadThread = new System.Threading.Thread(uploadJob);
uploadThread.Start();
}
catch (Exception e)
{
logMessage("Swallowing exception: " + e.Message);
}
}
///
/// Records a specific event as having occured and optionally records some attributes associated with this event.
/// This should not be called inside a loop. It should not be used to record personally identifiable information
/// and it is best to define all your event names rather than generate them programatically.
///
/// The name of the event which occured. E.G. 'button pressed'
/// Key value pairs that record data relevant to the event.
public void tagEvent(string eventName, Dictionary attributes = null)
{
if (this.isSessionOpen == false)
{
logMessage("Event not tagged because session is not open.");
return;
}
try
{
StringBuilder eventString = new StringBuilder();
eventString.Append(LocalyticsSession.CONTROLLER_EVENT);
eventString.Append(LocalyticsSession.ACTION_CREATE);
eventString.Append(LocalyticsSession.OBJECT_EVENT_DP);
eventString.Append(formatYAMLLine(LocalyticsSession.PARAM_APP_UUID, this.appKey, 3));
eventString.Append(formatYAMLLine(LocalyticsSession.PARAM_UUID, Guid.NewGuid().ToString(), 3));
eventString.Append(formatYAMLLine(LocalyticsSession.PARAM_SESSION_UUID, this.sessionUuid, 3));
eventString.Append(formatYAMLLine(LocalyticsSession.PARAM_CLIENT_TIME, GetDatestring(), 3));
eventString.Append(formatYAMLLine(LocalyticsSession.PARAM_EVENT_NAME, eventName, 3));
if (latitude != 0 && longitude != 0)
{
eventString.Append(formatYAMLLine("lat", latitude.ToString(), 3));
eventString.Append(formatYAMLLine("lng", longitude.ToString(), 3));
}
if (attributes != null)
{
eventString.Append(LocalyticsSession.EVENT_ATTRIBUTE);
foreach (string key in attributes.Keys)
{
eventString.Append(formatYAMLLine(key, attributes[key], 4));
}
}
appendTextToFile(eventString.ToString(), this.sessionFilename);
logMessage("Tagged event: " + escapestring(eventName));
}
catch (Exception e)
{
logMessage("Swallowing exception: " + e.Message);
}
}
///
/// (optional) Records the current location. All events recorded after this function is called
/// will report this location. Subsequent calls update the location for any events which follow
/// The latitude, use 0 if unknown
/// The longitude, use 0 if unknown
///
public void setLocation(double latitude, double longitude)
{
this.latitude = latitude;
this.longitude = longitude;
}
///
/// Debug method to view all the Localytics files stored on the disk at the moment.
///
public static void dumpAllFiles()
{
var store = getStore();
if (store.DirectoryExists(LocalyticsSession.directoryName) == false)
{
logMessage("Localytics dir does not exist.");
return;
}
string[] files = store.GetFileNames(LocalyticsSession.directoryName + @"\*");
foreach (string file in files)
{
logMessage("File===" + file);
string[] lines = GetFileContents(file).Split('\n');
foreach (string line in lines)
{
if (line.Length > 1)
{
logMessage(line.Substring(0, line.Length - 1));
}
}
logMessage("===");
}
}
#endregion
}