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 }