diff --git a/External/Localytics/LICENSE b/External/Localytics/LICENSE new file mode 100755 index 00000000..382d0766 --- /dev/null +++ b/External/Localytics/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2009, Char Software, Inc. d/b/a Localytics +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Neither the name of Char Software, Inc., Localytics nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY CHAR SOFTWARE, INC. D/B/A LOCALYTICS ''AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL CHAR SOFTWARE, INC. D/B/A LOCALYTICS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/External/Localytics/LocalyticsDatabase.h b/External/Localytics/LocalyticsDatabase.h old mode 100755 new mode 100644 index f159089d..6c6ba7da --- a/External/Localytics/LocalyticsDatabase.h +++ b/External/Localytics/LocalyticsDatabase.h @@ -1,11 +1,11 @@ // // LocalyticsDatabase.h -// Copyright (C) 2012 Char Software Inc., DBA Localytics -// +// Copyright (C) 2013 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. -// +// with this source code. +// // Please visit www.localytics.com for more information. #import @@ -18,9 +18,7 @@ sqlite3 *_databaseConnection; } -+ (LocalyticsDatabase *)sharedLocalyticsDatabase; - -- (NSUInteger)databaseSize; +- (unsigned long long)databaseSize; - (int)eventCount; - (NSTimeInterval)createdTimestamp; @@ -48,7 +46,7 @@ - (BOOL)vacuumIfRequired; - (NSTimeInterval)lastSessionStartTimestamp; -- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp; +- (BOOL)setLastSessionStartTimestamp:(NSTimeInterval)timestamp; - (BOOL)isOptedOut; - (BOOL)setOptedOut:(BOOL)optOut; @@ -58,8 +56,14 @@ - (NSString *)customDimension:(int)dimension; - (BOOL)setCustomDimension:(int)dimension value:(NSString *)value; -- (NSString *)customerId; -- (BOOL)setCustomerId:(NSString *)newCustomerId; +- (BOOL)setValueForIdentifier:(NSString *)identifierName value:(NSString *)value; +- (NSString *)valueForIdentifier:(NSString *)identifierName; +- (BOOL)deleteIdentifer:(NSString *)identifierName; +- (NSDictionary *)identifiers; + +- (BOOL)setFacebookAttribution:(NSString *)fbAttribution; +- (NSString *)facebookAttributionFromDb; +- (NSString *)facebookAttributionFromPasteboard; - (NSInteger)safeIntegerValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key; - (NSString *)safeStringValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key; diff --git a/External/Localytics/LocalyticsDatabase.m b/External/Localytics/LocalyticsDatabase.m old mode 100755 new mode 100644 index 2dc9216c..05e64c8e --- a/External/Localytics/LocalyticsDatabase.m +++ b/External/Localytics/LocalyticsDatabase.m @@ -1,915 +1,1141 @@ // // LocalyticsDatabase.m -// Copyright (C) 2012 Char Software Inc., DBA Localytics -// +// Copyright (C) 2013 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. -// +// with this source code. +// // Please visit www.localytics.com for more information. #import "LocalyticsDatabase.h" +#import "LocalyticsSession.h" #define LOCALYTICS_DIR @".localytics" // Name for the directory in which Localytics database is stored #define LOCALYTICS_DB @"localytics" // File name for the database (without extension) #define BUSY_TIMEOUT 30 // Maximum time SQlite will busy-wait for the database to unlock before returning SQLITE_BUSY @interface LocalyticsDatabase () - - (int)schemaVersion; - - (void)createSchema; - - (void)upgradeToSchemaV2; - - (void)upgradeToSchemaV3; - - (void)upgradeToSchemaV4; - - (void)upgradeToSchemaV5; - - (void)upgradeToSchemaV6; +- (int)schemaVersion; +- (void)createSchema; +- (void)upgradeToSchemaV2; +- (void)upgradeToSchemaV3; +- (void)upgradeToSchemaV4; +- (void)upgradeToSchemaV5; +- (void)upgradeToSchemaV6; - (void)upgradeToSchemaV7; - - (void)moveDbToCaches; - - (NSString *)randomUUID; +- (void)upgradeToSchemaV8; +- (void)upgradeToSchemaV9; +- (void)upgradeToSchemaV10; +- (void)upgradeToSchemaV11; +- (void)upgradeToSchemaV12; +- (void)moveDbToCaches; +- (NSString *)randomUUID; @end @implementation LocalyticsDatabase -// The singleton database object. -static LocalyticsDatabase *_sharedLocalyticsDatabase = nil; - + (NSString *)localyticsDirectoryPath { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - return [[paths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + return [[paths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR]; } + (NSString *)localyticsDatabasePath { - NSString *path = [[LocalyticsDatabase localyticsDirectoryPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", LOCALYTICS_DB]]; - return path; -} - -#pragma mark Singleton Class -+ (LocalyticsDatabase *)sharedLocalyticsDatabase { - @synchronized(self) { - if (_sharedLocalyticsDatabase == nil) { - _sharedLocalyticsDatabase = [[self alloc] init]; - } - } - return _sharedLocalyticsDatabase; + NSString *path = [[LocalyticsDatabase localyticsDirectoryPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", LOCALYTICS_DB]]; + return path; } - (LocalyticsDatabase *)init { if((self = [super init])) { - - // Mover any data that a previous library may have left in the documents directory - [self moveDbToCaches]; - - // Create directory structure for Localytics. - NSString *directoryPath = [LocalyticsDatabase localyticsDirectoryPath]; - if (![[NSFileManager defaultManager] fileExistsAtPath:directoryPath]) { - [[NSFileManager defaultManager] createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:nil]; - } - - // Attempt to open database. It will be created if it does not exist, already. - NSString *dbPath = [LocalyticsDatabase localyticsDatabasePath]; - int code = sqlite3_open([dbPath UTF8String], &_databaseConnection); - - // If we were unable to open the database, it is likely corrupted. Clobber it and move on. - if (code != SQLITE_OK) { - [[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil]; - code = sqlite3_open([dbPath UTF8String], &_databaseConnection); - } - - // Enable foreign key constraints. - if (code == SQLITE_OK) { - const char *sql = [@"PRAGMA foreign_keys = ON;" cStringUsingEncoding:NSUTF8StringEncoding]; - code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); - } - - // Check db connection, creating schema if necessary. - if (code == SQLITE_OK) { - sqlite3_busy_timeout(_databaseConnection, BUSY_TIMEOUT); // Defaults to 0, otherwise. - if ([self schemaVersion] == 0) { - [self createSchema]; - } - } - - // Perform any Migrations if necessary - if ([self schemaVersion] < 2) { - [self upgradeToSchemaV2]; - } - if ([self schemaVersion] < 3) { - [self upgradeToSchemaV3]; - } - if ([self schemaVersion] < 4) { - [self upgradeToSchemaV4]; - } - if ([self schemaVersion] < 5) { - [self upgradeToSchemaV5]; - } - if ([self schemaVersion] < 6) { - [self upgradeToSchemaV6]; - } - if ([self schemaVersion] < 7) { - [self upgradeToSchemaV7]; - } - } - + + // Mover any data that a previous library may have left in the documents directory + [self moveDbToCaches]; + + // Create directory structure for Localytics. + NSString *directoryPath = [LocalyticsDatabase localyticsDirectoryPath]; + if (![[NSFileManager defaultManager] fileExistsAtPath:directoryPath]) { + [[NSFileManager defaultManager] createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:nil]; + } + + // Attempt to open database. It will be created if it does not exist, already. + NSString *dbPath = [LocalyticsDatabase localyticsDatabasePath]; + int code = sqlite3_open([dbPath UTF8String], &_databaseConnection); + + // If we were unable to open the database, it is likely corrupted. Clobber it and move on. + if (code != SQLITE_OK) { + [[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil]; + code = sqlite3_open([dbPath UTF8String], &_databaseConnection); + } + + // Enable foreign key constraints. + if (code == SQLITE_OK) { + const char *sql = [@"PRAGMA foreign_keys = ON;" cStringUsingEncoding:NSUTF8StringEncoding]; + code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); + } + + // Check db connection, creating schema if necessary. + BOOL firstRun = NO; + if (code == SQLITE_OK) { + sqlite3_busy_timeout(_databaseConnection, BUSY_TIMEOUT); // Defaults to 0, otherwise. + if ([self schemaVersion] == 0) { + [self createSchema]; + firstRun = YES; + } + } + + // Perform any Migrations if necessary + if ([self schemaVersion] < 2) { + [self upgradeToSchemaV2]; + } + if ([self schemaVersion] < 3) { + [self upgradeToSchemaV3]; + } + if ([self schemaVersion] < 4) { + [self upgradeToSchemaV4]; + } + if ([self schemaVersion] < 5) { + [self upgradeToSchemaV5]; + } + if ([self schemaVersion] < 6) { + [self upgradeToSchemaV6]; + } + if ([self schemaVersion] < 7) { + [self upgradeToSchemaV7]; + } + if ([self schemaVersion] < 8) { + [self upgradeToSchemaV8]; + } + if ([self schemaVersion] < 9) { + [self upgradeToSchemaV9]; + } + if ([self schemaVersion] < 10) { + [self upgradeToSchemaV10]; + } + if ([self schemaVersion] < 11) { + [self upgradeToSchemaV11]; + } + if ([self schemaVersion] < 12) { + [self upgradeToSchemaV12]; + } + + // Perfrorm first run actions + if(firstRun) + { + [self collectFacebookAttributionIfAvailable]; + } + } + return self; } -#pragma mark - Database +#pragma mark - Database - (BOOL)beginTransaction:(NSString *)name { - const char *sql = [[NSString stringWithFormat:@"SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding]; - int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); - return code == SQLITE_OK; + const char *sql = [[NSString stringWithFormat:@"SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding]; + int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); + return code == SQLITE_OK; } - (BOOL)releaseTransaction:(NSString *)name { - const char *sql = [[NSString stringWithFormat:@"RELEASE SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding]; - int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); - return code == SQLITE_OK; + const char *sql = [[NSString stringWithFormat:@"RELEASE SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding]; + int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); + return code == SQLITE_OK; } - (BOOL)rollbackTransaction:(NSString *)name { - const char *sql = [[NSString stringWithFormat:@"ROLLBACK SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding]; - int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); - return code == SQLITE_OK; + const char *sql = [[NSString stringWithFormat:@"ROLLBACK SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding]; + int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL); + return code == SQLITE_OK; } - (int)schemaVersion { - int version = 0; - const char *sql = "SELECT MAX(schema_version) FROM localytics_info"; - sqlite3_stmt *selectSchemaVersion; - if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectSchemaVersion, NULL) == SQLITE_OK) { - if(sqlite3_step(selectSchemaVersion) == SQLITE_ROW) { - version = sqlite3_column_int(selectSchemaVersion, 0); - } - } - sqlite3_finalize(selectSchemaVersion); - return version; + int version = 0; + const char *sql = "SELECT MAX(schema_version) FROM localytics_info"; + sqlite3_stmt *selectSchemaVersion; + if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectSchemaVersion, NULL) == SQLITE_OK) { + if(sqlite3_step(selectSchemaVersion) == SQLITE_ROW) { + version = sqlite3_column_int(selectSchemaVersion, 0); + } + } + sqlite3_finalize(selectSchemaVersion); + return version; } - (NSString *)installId { - NSString *installId = nil; - - sqlite3_stmt *selectInstallId; - sqlite3_prepare_v2(_databaseConnection, "SELECT install_id FROM localytics_info", -1, &selectInstallId, NULL); - int code = sqlite3_step(selectInstallId); - if (code == SQLITE_ROW && sqlite3_column_text(selectInstallId, 0)) { - installId = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectInstallId, 0)]; - } - sqlite3_finalize(selectInstallId); - - return installId; + NSString *installId = nil; + + sqlite3_stmt *selectInstallId; + sqlite3_prepare_v2(_databaseConnection, "SELECT install_id FROM localytics_info", -1, &selectInstallId, NULL); + int code = sqlite3_step(selectInstallId); + if (code == SQLITE_ROW && sqlite3_column_text(selectInstallId, 0)) { + installId = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectInstallId, 0)]; + } + sqlite3_finalize(selectInstallId); + + return installId; } - (NSString *)appKey { - NSString *appKey = nil; - - sqlite3_stmt *selectAppKey; - sqlite3_prepare_v2(_databaseConnection, "SELECT app_key FROM localytics_info", -1, &selectAppKey, NULL); - int code = sqlite3_step(selectAppKey); - if (code == SQLITE_ROW && sqlite3_column_text(selectAppKey, 0)) { - appKey = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectAppKey, 0)]; - } - sqlite3_finalize(selectAppKey); - - return appKey; + NSString *appKey = nil; + + sqlite3_stmt *selectAppKey; + sqlite3_prepare_v2(_databaseConnection, "SELECT app_key FROM localytics_info", -1, &selectAppKey, NULL); + int code = sqlite3_step(selectAppKey); + if (code == SQLITE_ROW && sqlite3_column_text(selectAppKey, 0)) { + appKey = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectAppKey, 0)]; + } + sqlite3_finalize(selectAppKey); + + return appKey; } // Due to the new iOS storage guidelines it is necessary to move the database out of the documents directory -// and into the /library/caches directory +// and into the /library/caches directory - (void)moveDbToCaches { - NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *localyticsDocumentsDirectory = [[documentPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR]; - NSArray *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString *localyticsCachesDirectory = [[cachesPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR]; - - // If the old directory doesn't exist, there is nothing else to do here - if([[NSFileManager defaultManager] fileExistsAtPath:localyticsDocumentsDirectory] == NO) - { - return; - } - - // Try to move the directory - if(NO == [[NSFileManager defaultManager] moveItemAtPath:localyticsDocumentsDirectory - toPath:localyticsCachesDirectory - error:nil]) - { - // If the move failed try and, delete the old directory - [ [NSFileManager defaultManager] removeItemAtPath:localyticsDocumentsDirectory error:nil]; - } + NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *localyticsDocumentsDirectory = [[documentPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR]; + NSArray *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *localyticsCachesDirectory = [[cachesPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR]; + + // If the old directory doesn't exist, there is nothing else to do here + if([[NSFileManager defaultManager] fileExistsAtPath:localyticsDocumentsDirectory] == NO) + { + return; + } + + // Try to move the directory + if(NO == [[NSFileManager defaultManager] moveItemAtPath:localyticsDocumentsDirectory + toPath:localyticsCachesDirectory + error:nil]) + { + // If the move failed try and, delete the old directory + [ [NSFileManager defaultManager] removeItemAtPath:localyticsDocumentsDirectory error:nil]; + } } - (void)createSchema { - int code = SQLITE_OK; - - // Execute schema creation within a single transaction. - code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "CREATE TABLE upload_headers (" - "sequence_number INTEGER PRIMARY KEY, " - "blob_string TEXT)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "CREATE TABLE events (" - "event_id INTEGER PRIMARY KEY AUTOINCREMENT, " // In case foreign key constraints are reintroduced. - "upload_header INTEGER, " - "blob_string TEXT NOT NULL)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "CREATE TABLE localytics_info (" - "schema_version INTEGER PRIMARY KEY, " - "last_upload_number INTEGER, " - "last_session_number INTEGER, " - "opt_out BOOLEAN, " - "last_close_event INTEGER, " - "last_flow_event INTEGER, " - "last_session_start REAL, " - "app_key CHAR(64), " - "custom_d0 CHAR(64), " - "custom_d1 CHAR(64), " - "custom_d2 CHAR(64), " - "custom_d3 CHAR(64) " - ")", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "INSERT INTO localytics_info (schema_version, last_upload_number, last_session_number, opt_out) " - "VALUES (3, 0, 0, 0)", NULL, NULL, NULL); - } - - // Commit transaction. - if (code == SQLITE_OK || code == SQLITE_DONE) { - sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); - } else { - sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); - } + int code = SQLITE_OK; + + // Execute schema creation within a single transaction. + code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "CREATE TABLE upload_headers (" + "sequence_number INTEGER PRIMARY KEY, " + "blob_string TEXT)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "CREATE TABLE events (" + "event_id INTEGER PRIMARY KEY AUTOINCREMENT, " // In case foreign key constraints are reintroduced. + "upload_header INTEGER, " + "blob_string TEXT NOT NULL)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "CREATE TABLE localytics_info (" + "schema_version INTEGER PRIMARY KEY, " + "last_upload_number INTEGER, " + "last_session_number INTEGER, " + "opt_out BOOLEAN, " + "last_close_event INTEGER, " + "last_flow_event INTEGER, " + "last_session_start REAL, " + "app_key CHAR(64), " + "custom_d0 CHAR(64), " + "custom_d1 CHAR(64), " + "custom_d2 CHAR(64), " + "custom_d3 CHAR(64) " + ")", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "INSERT INTO localytics_info (schema_version, last_upload_number, last_session_number, opt_out) " + "VALUES (3, 0, 0, 0)", NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } // V2 adds a unique identifier for each installation // This identifier has been moved to user preferences so the database an live in the caches directory // Also adds storage for custom dimensions - (void)upgradeToSchemaV2 { - int code = SQLITE_OK; - - code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD install_id CHAR(40)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD custom_d0 CHAR(64)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD custom_d1 CHAR(64)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD custom_d2 CHAR(64)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD custom_d3 CHAR(64)", - NULL, NULL, NULL); - } - - // Attempt to set schema version and install_id regardless of the result code following the ALTER statements above. - // This is necessary because a previous version of the library performed the migration without setting these values. - // The transaction will succeed even if the individual statements fail with errors (eg. "duplicate column name"). - sqlite3_stmt *updateLocalyticsInfo; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set install_id = ?, schema_version = 2 ", -1, &updateLocalyticsInfo, NULL); - sqlite3_bind_text (updateLocalyticsInfo, 1, [[self randomUUID] UTF8String], -1, SQLITE_TRANSIENT); - code = sqlite3_step(updateLocalyticsInfo); - sqlite3_finalize(updateLocalyticsInfo); - - // Commit transaction. - if (code == SQLITE_OK || code == SQLITE_DONE) { - sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); - } else { - sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); - } + int code = SQLITE_OK; + + code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD install_id CHAR(40)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d0 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d1 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d2 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d3 CHAR(64)", + NULL, NULL, NULL); + } + + // Attempt to set schema version and install_id regardless of the result code following the ALTER statements above. + // This is necessary because a previous version of the library performed the migration without setting these values. + // The transaction will succeed even if the individual statements fail with errors (eg. "duplicate column name"). + sqlite3_stmt *updateLocalyticsInfo; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set install_id = ?, schema_version = 2 ", -1, &updateLocalyticsInfo, NULL); + sqlite3_bind_text (updateLocalyticsInfo, 1, [[self randomUUID] UTF8String], -1, SQLITE_TRANSIENT); + code = sqlite3_step(updateLocalyticsInfo); + sqlite3_finalize(updateLocalyticsInfo); + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } // V3 adds a field for the last app key and patches a V2 migration issue. - (void)upgradeToSchemaV3 { - int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); - - if (code == SQLITE_OK) { - sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD app_key CHAR(64)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "UPDATE localytics_info set schema_version = 3", - NULL, NULL, NULL); - } - - // Commit transaction. - if (code == SQLITE_OK || code == SQLITE_DONE) { - sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); - } else { - sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); - } + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD app_key CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 3", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } // V4 adds a field for the customer id. - (void)upgradeToSchemaV4 { - int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); - - if (code == SQLITE_OK) { - sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD customer_id CHAR(64)", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "UPDATE localytics_info set schema_version = 4", - NULL, NULL, NULL); - } - - // Commit transaction. - if (code == SQLITE_OK || code == SQLITE_DONE) { - sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); - } else { - sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); - } + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD customer_id CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 4", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } // V5 adds AMP related tables. - (void)upgradeToSchemaV5 { - - int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); - //The AMP DB table was initially created here. in Version 7 it will be dropped and re-added with the correct data types. + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + //The AMP DB table was initially created here. in Version 7 it will be dropped and re-added with the correct data types. //therefore the code that creates it is no longer going to be called here. - + //we still want to change the schema version - - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "UPDATE localytics_info set schema_version = 5", - NULL, NULL, NULL); - } - - // Commit transaction. - if (code == SQLITE_OK || code == SQLITE_DONE) { - sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); - } else { - sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); - } + + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 5", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } // V6 adds a field for the queued close event blob string. - (void)upgradeToSchemaV6 { - int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); - - if (code == SQLITE_OK) { - sqlite3_exec(_databaseConnection, - "ALTER TABLE localytics_info ADD queued_close_event_blob TEXT", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "UPDATE localytics_info set schema_version = 6", - NULL, NULL, NULL); - } - - // Commit transaction. - if (code == SQLITE_OK || code == SQLITE_DONE) { - sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); - } else { - sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); - } + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD queued_close_event_blob TEXT", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 6", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } --(void)upgradeToSchemaV7 { - int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); - +- (void)upgradeToSchemaV7 { + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + if (code == SQLITE_OK) { code = sqlite3_exec(_databaseConnection, "DROP TABLE IF EXISTS localytics_amp_rule", NULL, NULL, NULL); } - + if (code == SQLITE_OK) { code = sqlite3_exec(_databaseConnection, - "CREATE TABLE IF NOT EXISTS localytics_amp_rule (" - "rule_id INTEGER PRIMARY KEY AUTOINCREMENT, " - "rule_name TEXT UNIQUE, " - "expiration INTEGER, " - "phone_location TEXT, " - "phone_size_width INTEGER, " - "phone_size_height INTEGER, " - "tablet_location TEXT, " - "tablet_size_width INTEGER, " - "tablet_size_height INTEGER, " - "display_seconds INTEGER, " - "display_session INTEGER, " - "version INTEGER, " - "did_display INTEGER, " - "times_to_display INTEGER, " - "internet_required INTEGER, " - "ab_test TEXT" - ")", - NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "CREATE TABLE IF NOT EXISTS localytics_amp_ruleevent (" - "rule_id INTEGER, " - "event_name TEXT, " - "FOREIGN KEY(rule_id) REFERENCES localytics_amp_rule(rule_id) ON DELETE CASCADE " - ")", - NULL, NULL, NULL); + "CREATE TABLE IF NOT EXISTS localytics_amp_rule (" + "rule_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "rule_name TEXT UNIQUE, " + "expiration INTEGER, " + "phone_location TEXT, " + "phone_size_width INTEGER, " + "phone_size_height INTEGER, " + "tablet_location TEXT, " + "tablet_size_width INTEGER, " + "tablet_size_height INTEGER, " + "display_seconds INTEGER, " + "display_session INTEGER, " + "version INTEGER, " + "did_display INTEGER, " + "times_to_display INTEGER, " + "internet_required INTEGER, " + "ab_test TEXT" + ")", + NULL, NULL, NULL); } - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, - "UPDATE localytics_info set schema_version = 7", - NULL, NULL, NULL); - } - - // Commit transaction. - if (code == SQLITE_OK || code == SQLITE_DONE) { - sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); - } else { - sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); - } + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "CREATE TABLE IF NOT EXISTS localytics_amp_ruleevent (" + "rule_id INTEGER, " + "event_name TEXT, " + "FOREIGN KEY(rule_id) REFERENCES localytics_amp_rule(rule_id) ON DELETE CASCADE " + ")", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 7", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } -- (NSUInteger)databaseSize { - NSUInteger size = 0; - NSDictionary *fileAttributes = [[NSFileManager defaultManager] - attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath] - error:nil]; - size = [fileAttributes fileSize]; - return size; +- (void)upgradeToSchemaV8 { + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_amp_rule ADD campaign_id INTEGER", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_amp_rule ADD ttl_expiration INTEGER", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_amp_rule ADD update_on_ttl_expiration INTEGER", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 8", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } } -- (int) eventCount { - int count = 0; - const char *sql = "SELECT count(*) FROM events"; - sqlite3_stmt *selectEventCount; - - if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectEventCount, NULL) == SQLITE_OK) - { - if(sqlite3_step(selectEventCount) == SQLITE_ROW) { - count = sqlite3_column_int(selectEventCount, 0); - } - } - sqlite3_finalize(selectEventCount); - - return count; +- (void)upgradeToSchemaV9 { + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_amp_rule ADD location TEXT", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 9", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } +} + +- (void)upgradeToSchemaV10 { + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_amp_rule ADD conversion_expiration INTEGER", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "CREATE TABLE IF NOT EXISTS localytics_identifiers (" + "key TEXT PRIMARY KEY, " + "value TEXT" + ")", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 10", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } +} + +- (void)upgradeToSchemaV11 { + + int code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD fb_attribution TEXT", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_amp_rule ADD devices TEXT", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "CREATE TABLE IF NOT EXISTS localytics_amp_conditions (" + "condition_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "rule_id INTEGER, " + "attribute_name TEXT, " + "operator TEXT, " + "FOREIGN KEY(rule_id) REFERENCES localytics_amp_rule(rule_id) ON DELETE CASCADE " + ")", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "CREATE TABLE IF NOT EXISTS localytics_amp_conditions_values (" + "condition_id INTEGER, " + "value TEXT, " + "FOREIGN KEY(condition_id) REFERENCES localytics_amp_conditions(condition_id) ON DELETE CASCADE " + ")", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 11", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } +} + +- (void)upgradeToSchemaV12 { + int code = SQLITE_OK; + + code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL); + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d4 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d5 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d6 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d7 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d8 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD custom_d9 CHAR(64)", + NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info set schema_version = 12", + NULL, NULL, NULL); + } + + // Commit transaction. + if (code == SQLITE_OK || code == SQLITE_DONE) { + sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL); + } else { + sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL); + } +} + + +- (unsigned long long)databaseSize { + unsigned long long size = 0; + NSDictionary *fileAttributes = [[NSFileManager defaultManager] + attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath] + error:nil]; + size = [fileAttributes fileSize]; + return size; +} + +- (int)eventCount { + int count = 0; + const char *sql = "SELECT count(*) FROM events"; + sqlite3_stmt *selectEventCount; + + if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectEventCount, NULL) == SQLITE_OK) + { + if(sqlite3_step(selectEventCount) == SQLITE_ROW) { + count = sqlite3_column_int(selectEventCount, 0); + } + } + sqlite3_finalize(selectEventCount); + + return count; } - (NSTimeInterval)createdTimestamp { - NSTimeInterval timestamp = 0; - NSDictionary *fileAttributes = [[NSFileManager defaultManager] - attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath] - error:nil]; - timestamp = [[fileAttributes fileCreationDate] timeIntervalSince1970]; - return timestamp; + NSTimeInterval timestamp = 0; + NSDictionary *fileAttributes = [[NSFileManager defaultManager] + attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath] + error:nil]; + timestamp = [[fileAttributes fileCreationDate] timeIntervalSince1970]; + return timestamp; } - (NSTimeInterval)lastSessionStartTimestamp { - - NSTimeInterval lastSessionStart = 0; - - sqlite3_stmt *selectLastSessionStart; - sqlite3_prepare_v2(_databaseConnection, "SELECT last_session_start FROM localytics_info", -1, &selectLastSessionStart, NULL); - int code = sqlite3_step(selectLastSessionStart); - if (code == SQLITE_ROW) { - lastSessionStart = sqlite3_column_double(selectLastSessionStart, 0); - } - sqlite3_finalize(selectLastSessionStart); - - return lastSessionStart; + + NSTimeInterval lastSessionStart = 0; + + sqlite3_stmt *selectLastSessionStart; + sqlite3_prepare_v2(_databaseConnection, "SELECT last_session_start FROM localytics_info", -1, &selectLastSessionStart, NULL); + int code = sqlite3_step(selectLastSessionStart); + if (code == SQLITE_ROW) { + lastSessionStart = sqlite3_column_double(selectLastSessionStart, 0); + } + sqlite3_finalize(selectLastSessionStart); + + return lastSessionStart; } -- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp { - sqlite3_stmt *updateLastSessionStart; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_session_start = ?", -1, &updateLastSessionStart, NULL); - sqlite3_bind_double(updateLastSessionStart, 1, timestamp); - int code = sqlite3_step(updateLastSessionStart); - sqlite3_finalize(updateLastSessionStart); - - return code == SQLITE_DONE; +- (BOOL)setLastSessionStartTimestamp:(NSTimeInterval)timestamp { + sqlite3_stmt *updateLastSessionStart; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_session_start = ?", -1, &updateLastSessionStart, NULL); + sqlite3_bind_double(updateLastSessionStart, 1, timestamp); + int code = sqlite3_step(updateLastSessionStart); + sqlite3_finalize(updateLastSessionStart); + + return code == SQLITE_DONE; } -- (BOOL)isOptedOut { - BOOL optedOut = NO; - - sqlite3_stmt *selectOptOut; - sqlite3_prepare_v2(_databaseConnection, "SELECT opt_out FROM localytics_info", -1, &selectOptOut, NULL); - int code = sqlite3_step(selectOptOut); - if (code == SQLITE_ROW) { - optedOut = sqlite3_column_int(selectOptOut, 0) == 1; - } - sqlite3_finalize(selectOptOut); - - return optedOut; +- (BOOL)isOptedOut { + BOOL optedOut = NO; + + sqlite3_stmt *selectOptOut; + sqlite3_prepare_v2(_databaseConnection, "SELECT opt_out FROM localytics_info", -1, &selectOptOut, NULL); + int code = sqlite3_step(selectOptOut); + if (code == SQLITE_ROW) { + optedOut = sqlite3_column_int(selectOptOut, 0) == 1; + } + sqlite3_finalize(selectOptOut); + + return optedOut; } - (BOOL)setOptedOut:(BOOL)optOut { - sqlite3_stmt *updateOptedOut; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET opt_out = ?", -1, &updateOptedOut, NULL); - sqlite3_bind_int(updateOptedOut, 1, optOut); - int code = sqlite3_step(updateOptedOut); - sqlite3_finalize(updateOptedOut); - - return code == SQLITE_OK; + sqlite3_stmt *updateOptedOut; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET opt_out = ?", -1, &updateOptedOut, NULL); + sqlite3_bind_int(updateOptedOut, 1, optOut); + int code = sqlite3_step(updateOptedOut); + sqlite3_finalize(updateOptedOut); + + return code == SQLITE_OK; } - (NSString *)customDimension:(int)dimension { - if(dimension < 0 || dimension > 3) { - return nil; - } - - NSString *value = nil; - NSString *query = [NSString stringWithFormat:@"select custom_d%i from localytics_info", dimension]; - - sqlite3_stmt *selectCustomDim; - sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectCustomDim, NULL); - int code = sqlite3_step(selectCustomDim); - if (code == SQLITE_ROW && sqlite3_column_text(selectCustomDim, 0)) { - value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectCustomDim, 0)]; - } - sqlite3_finalize(selectCustomDim); - - return value; + if(dimension < 0 || dimension > 9) { + return nil; + } + + NSString *value = nil; + NSString *query = [NSString stringWithFormat:@"select custom_d%i from localytics_info", dimension]; + + sqlite3_stmt *selectCustomDim; + sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectCustomDim, NULL); + int code = sqlite3_step(selectCustomDim); + if (code == SQLITE_ROW && sqlite3_column_text(selectCustomDim, 0)) { + value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectCustomDim, 0)]; + } + sqlite3_finalize(selectCustomDim); + + return value; } - (BOOL)setCustomDimension:(int)dimension value:(NSString *)value { - if(dimension < 0 || dimension > 3) { - return false; - } - - NSString *query = [NSString stringWithFormat:@"update localytics_info SET custom_d%i = %@", - dimension, - (value == nil) ? @"null" : [NSString stringWithFormat:@"\"%@\"", value]]; - - int code = sqlite3_exec(_databaseConnection, [query UTF8String], NULL, NULL, NULL); - - return code == SQLITE_OK; + if(dimension < 0 || dimension > 9) { + return false; + } + + NSString *query = [NSString stringWithFormat:@"update localytics_info SET custom_d%i = %@", + dimension, + (value == nil) ? @"null" : [NSString stringWithFormat:@"\"%@\"", value]]; + + int code = sqlite3_exec(_databaseConnection, [query UTF8String], NULL, NULL, NULL); + + return code == SQLITE_OK; } - (BOOL)incrementLastUploadNumber:(int *)uploadNumber { - NSString *t = @"increment_upload_number"; - int code = SQLITE_OK; - - code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; - - if(code == SQLITE_OK) { - // Increment value - code = sqlite3_exec(_databaseConnection, - "UPDATE localytics_info " - "SET last_upload_number = (last_upload_number + 1)", - NULL, NULL, NULL); - } - - if(code == SQLITE_OK) { - // Retrieve new value - sqlite3_stmt *selectUploadNumber; - sqlite3_prepare_v2(_databaseConnection, - "SELECT last_upload_number FROM localytics_info", - -1, &selectUploadNumber, NULL); - code = sqlite3_step(selectUploadNumber); - if (code == SQLITE_ROW) { - *uploadNumber = sqlite3_column_int(selectUploadNumber, 0); - } - sqlite3_finalize(selectUploadNumber); - } - - if(code == SQLITE_ROW) { - [self releaseTransaction:t]; - } else { - [self rollbackTransaction:t]; - } - - return code == SQLITE_ROW; + NSString *t = @"increment_upload_number"; + int code = SQLITE_OK; + + code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; + + if(code == SQLITE_OK) { + // Increment value + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info " + "SET last_upload_number = (last_upload_number + 1)", + NULL, NULL, NULL); + } + + if(code == SQLITE_OK) { + // Retrieve new value + sqlite3_stmt *selectUploadNumber; + sqlite3_prepare_v2(_databaseConnection, + "SELECT last_upload_number FROM localytics_info", + -1, &selectUploadNumber, NULL); + code = sqlite3_step(selectUploadNumber); + if (code == SQLITE_ROW) { + *uploadNumber = sqlite3_column_int(selectUploadNumber, 0); + } + sqlite3_finalize(selectUploadNumber); + } + + if(code == SQLITE_ROW) { + [self releaseTransaction:t]; + } else { + [self rollbackTransaction:t]; + } + + return code == SQLITE_ROW; } - (BOOL)incrementLastSessionNumber:(int *)sessionNumber { - NSString *t = @"increment_session_number"; - int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; - - if(code == SQLITE_OK) { - // Increment value - code = sqlite3_exec(_databaseConnection, - "UPDATE localytics_info " - "SET last_session_number = (last_session_number + 1)", - NULL, NULL, NULL); - } - - if(code == SQLITE_OK) { - // Retrieve new value - sqlite3_stmt *selectSessionNumber; - sqlite3_prepare_v2(_databaseConnection, - "SELECT last_session_number FROM localytics_info", - -1, &selectSessionNumber, NULL); - code = sqlite3_step(selectSessionNumber); - if (code == SQLITE_ROW && sessionNumber != NULL) { - *sessionNumber = sqlite3_column_int(selectSessionNumber, 0); - } - sqlite3_finalize(selectSessionNumber); - } - - if(code == SQLITE_ROW) { - [self releaseTransaction:t]; - } else { - [self rollbackTransaction:t]; - } - - return code == SQLITE_ROW; + NSString *t = @"increment_session_number"; + int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; + + if(code == SQLITE_OK) { + // Increment value + code = sqlite3_exec(_databaseConnection, + "UPDATE localytics_info " + "SET last_session_number = (last_session_number + 1)", + NULL, NULL, NULL); + } + + if(code == SQLITE_OK) { + // Retrieve new value + sqlite3_stmt *selectSessionNumber; + sqlite3_prepare_v2(_databaseConnection, + "SELECT last_session_number FROM localytics_info", + -1, &selectSessionNumber, NULL); + code = sqlite3_step(selectSessionNumber); + if (code == SQLITE_ROW && sessionNumber != NULL) { + *sessionNumber = sqlite3_column_int(selectSessionNumber, 0); + } + sqlite3_finalize(selectSessionNumber); + } + + if(code == SQLITE_ROW) { + [self releaseTransaction:t]; + } else { + [self rollbackTransaction:t]; + } + + return code == SQLITE_ROW; } - (BOOL)addEventWithBlobString:(NSString *)blob { - - int code = SQLITE_OK; - sqlite3_stmt *insertEvent; - sqlite3_prepare_v2(_databaseConnection, "INSERT INTO events (blob_string) VALUES (?)", -1, &insertEvent, NULL); - sqlite3_bind_text(insertEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT); - code = sqlite3_step(insertEvent); - sqlite3_finalize(insertEvent); - - return code == SQLITE_DONE; + + int code = SQLITE_OK; + sqlite3_stmt *insertEvent; + sqlite3_prepare_v2(_databaseConnection, "INSERT INTO events (blob_string) VALUES (?)", -1, &insertEvent, NULL); + sqlite3_bind_text(insertEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT); + code = sqlite3_step(insertEvent); + sqlite3_finalize(insertEvent); + + return code == SQLITE_DONE; } - (BOOL)addCloseEventWithBlobString:(NSString *)blob { - NSString *t = @"add_close_event"; - BOOL success = [self beginTransaction:t]; - - // Add close event. - if (success) { - success = [self addEventWithBlobString:blob]; - } - - // Record row id to localytics_info so that it can be removed if the session resumes. - if (success) { - sqlite3_stmt *updateCloseEvent; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_close_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateCloseEvent, NULL); - sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection); - sqlite3_bind_int64(updateCloseEvent, 1, lastRow); - int code = sqlite3_step(updateCloseEvent); - sqlite3_finalize(updateCloseEvent); - success = code == SQLITE_DONE; - } - - if (success) { - [self releaseTransaction:t]; - } else { - [self rollbackTransaction:t]; - } - return success; + NSString *t = @"add_close_event"; + BOOL success = [self beginTransaction:t]; + + // Add close event. + if (success) { + success = [self addEventWithBlobString:blob]; + } + + // Record row id to localytics_info so that it can be removed if the session resumes. + if (success) { + sqlite3_stmt *updateCloseEvent; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_close_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateCloseEvent, NULL); + sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection); + sqlite3_bind_int64(updateCloseEvent, 1, lastRow); + int code = sqlite3_step(updateCloseEvent); + sqlite3_finalize(updateCloseEvent); + success = code == SQLITE_DONE; + } + + if (success) { + [self releaseTransaction:t]; + } else { + [self rollbackTransaction:t]; + } + return success; } - (BOOL)queueCloseEventWithBlobString:(NSString *)blob { - NSString *t = @"queue_close_event"; - BOOL success = [self beginTransaction:t]; - - // Queue close event. - if (success) { - sqlite3_stmt *queueCloseEvent; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET queued_close_event_blob = ?", -1, &queueCloseEvent, NULL); - sqlite3_bind_text(queueCloseEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT); - int code = sqlite3_step(queueCloseEvent); - sqlite3_finalize(queueCloseEvent); - success = code == SQLITE_DONE; - } - - if (success) { - [self releaseTransaction:t]; - } else { - [self rollbackTransaction:t]; - } - return success; + NSString *t = @"queue_close_event"; + BOOL success = [self beginTransaction:t]; + + // Queue close event. + if (success) { + sqlite3_stmt *queueCloseEvent; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET queued_close_event_blob = ?", -1, &queueCloseEvent, NULL); + sqlite3_bind_text(queueCloseEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT); + int code = sqlite3_step(queueCloseEvent); + sqlite3_finalize(queueCloseEvent); + success = code == SQLITE_DONE; + } + + if (success) { + [self releaseTransaction:t]; + } else { + [self rollbackTransaction:t]; + } + return success; } - (NSString *)dequeueCloseEventBlobString { - NSString *value = nil; - NSString *query = @"SELECT queued_close_event_blob FROM localytics_info"; - - sqlite3_stmt *selectStmt; - sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectStmt, NULL); - int code = sqlite3_step(selectStmt); - if (code == SQLITE_ROW && sqlite3_column_text(selectStmt, 0)) { - value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectStmt, 0)]; - } - sqlite3_finalize(selectStmt); - - // Clear the queued close event blob. - [self queueCloseEventWithBlobString:nil]; - - return value; + NSString *value = nil; + NSString *query = @"SELECT queued_close_event_blob FROM localytics_info"; + + sqlite3_stmt *selectStmt; + sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectStmt, NULL); + int code = sqlite3_step(selectStmt); + if (code == SQLITE_ROW && sqlite3_column_text(selectStmt, 0)) { + value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectStmt, 0)]; + } + sqlite3_finalize(selectStmt); + + // Clear the queued close event blob. + [self queueCloseEventWithBlobString:nil]; + + return value; } - (BOOL)addFlowEventWithBlobString:(NSString *)blob { - NSString *t = @"add_flow_event"; - BOOL success = [self beginTransaction:t]; - - // Add flow event. - if (success) { - success = [self addEventWithBlobString:blob]; - } - - // Record row id to localytics_info so that it can be removed if the session resumes. - if (success) { - sqlite3_stmt *updateFlowEvent; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_flow_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateFlowEvent, NULL); - sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection); - sqlite3_bind_int64(updateFlowEvent, 1, lastRow); - int code = sqlite3_step(updateFlowEvent); - sqlite3_finalize(updateFlowEvent); - success = code == SQLITE_DONE; - } - - if (success) { - [self releaseTransaction:t]; - } else { - [self rollbackTransaction:t]; - } - return success; + NSString *t = @"add_flow_event"; + BOOL success = [self beginTransaction:t]; + + // Add flow event. + if (success) { + success = [self addEventWithBlobString:blob]; + } + + // Record row id to localytics_info so that it can be removed if the session resumes. + if (success) { + sqlite3_stmt *updateFlowEvent; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_flow_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateFlowEvent, NULL); + sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection); + sqlite3_bind_int64(updateFlowEvent, 1, lastRow); + int code = sqlite3_step(updateFlowEvent); + sqlite3_finalize(updateFlowEvent); + success = code == SQLITE_DONE; + } + + if (success) { + [self releaseTransaction:t]; + } else { + [self rollbackTransaction:t]; + } + return success; } - (BOOL)removeLastCloseAndFlowEvents { - // Attempt to remove the last recorded close event. - // Fail quietly if none was saved or it was previously removed. - int code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE event_id = (SELECT last_close_event FROM localytics_info) OR event_id = (SELECT last_flow_event FROM localytics_info)", NULL, NULL, NULL); - - return code == SQLITE_OK; + // Attempt to remove the last recorded close event. + // Fail quietly if none was saved or it was previously removed. + int code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE event_id = (SELECT last_close_event FROM localytics_info) OR event_id = (SELECT last_flow_event FROM localytics_info)", NULL, NULL, NULL); + + return code == SQLITE_OK; } - (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId { - sqlite3_stmt *insertHeader; - sqlite3_prepare_v2(_databaseConnection, "INSERT INTO upload_headers (sequence_number, blob_string) VALUES (?, ?)", -1, &insertHeader, NULL); - sqlite3_bind_int(insertHeader, 1, number); - sqlite3_bind_text(insertHeader, 2, [blob UTF8String], -1, SQLITE_TRANSIENT); - int code = sqlite3_step(insertHeader); - sqlite3_finalize(insertHeader); - - if (code == SQLITE_DONE && insertedRowId != NULL) { - *insertedRowId = sqlite3_last_insert_rowid(_databaseConnection); - } - - return code == SQLITE_DONE; + sqlite3_stmt *insertHeader; + sqlite3_prepare_v2(_databaseConnection, "INSERT INTO upload_headers (sequence_number, blob_string) VALUES (?, ?)", -1, &insertHeader, NULL); + sqlite3_bind_int(insertHeader, 1, number); + sqlite3_bind_text(insertHeader, 2, [blob UTF8String], -1, SQLITE_TRANSIENT); + int code = sqlite3_step(insertHeader); + sqlite3_finalize(insertHeader); + + if (code == SQLITE_DONE && insertedRowId != NULL) { + *insertedRowId = sqlite3_last_insert_rowid(_databaseConnection); + } + + return code == SQLITE_DONE; } - (int)unstagedEventCount { - int rowCount = 0; - sqlite3_stmt *selectEventCount; - sqlite3_prepare_v2(_databaseConnection, "SELECT COUNT(*) FROM events WHERE UPLOAD_HEADER IS NULL", -1, &selectEventCount, NULL); - int code = sqlite3_step(selectEventCount); - if (code == SQLITE_ROW) { - rowCount = sqlite3_column_int(selectEventCount, 0); - } - sqlite3_finalize(selectEventCount); - - return rowCount; + int rowCount = 0; + sqlite3_stmt *selectEventCount; + sqlite3_prepare_v2(_databaseConnection, "SELECT COUNT(*) FROM events WHERE UPLOAD_HEADER IS NULL", -1, &selectEventCount, NULL); + int code = sqlite3_step(selectEventCount); + if (code == SQLITE_ROW) { + rowCount = sqlite3_column_int(selectEventCount, 0); + } + sqlite3_finalize(selectEventCount); + + return rowCount; } - (BOOL)stageEventsForUpload:(sqlite3_int64)headerId { - - // Associate all outstanding events with the given upload header ID. - NSString *stageEvents = [NSString stringWithFormat:@"UPDATE events SET upload_header = ? WHERE upload_header IS NULL"]; - sqlite3_stmt *updateEvents; - sqlite3_prepare_v2(_databaseConnection, [stageEvents UTF8String], -1, &updateEvents, NULL); - sqlite3_bind_int(updateEvents, 1, headerId); - int code = sqlite3_step(updateEvents); - sqlite3_finalize(updateEvents); - BOOL success = (code == SQLITE_DONE); - - return success; + + // Associate all outstanding events with the given upload header ID. + NSString *stageEvents = [NSString stringWithFormat:@"UPDATE events SET upload_header = ? WHERE upload_header IS NULL"]; + sqlite3_stmt *updateEvents; + sqlite3_prepare_v2(_databaseConnection, [stageEvents UTF8String], -1, &updateEvents, NULL); + sqlite3_bind_int64(updateEvents, 1, headerId); + int code = sqlite3_step(updateEvents); + sqlite3_finalize(updateEvents); + BOOL success = (code == SQLITE_DONE); + + return success; } - (BOOL)updateAppKey:(NSString *)appKey { - sqlite3_stmt *updateAppKey; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set app_key = ?", -1, &updateAppKey, NULL); - sqlite3_bind_text (updateAppKey, 1, [appKey UTF8String], -1, SQLITE_TRANSIENT); - int code = sqlite3_step(updateAppKey); - sqlite3_finalize(updateAppKey); - BOOL success = (code == SQLITE_DONE); - - return success; + sqlite3_stmt *updateAppKey; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set app_key = ?", -1, &updateAppKey, NULL); + sqlite3_bind_text (updateAppKey, 1, [appKey UTF8String], -1, SQLITE_TRANSIENT); + int code = sqlite3_step(updateAppKey); + sqlite3_finalize(updateAppKey); + BOOL success = (code == SQLITE_DONE); + + return success; } - (NSString *)uploadBlobString { - - // Retrieve the blob strings of each upload header and its child events, in order. - const char *sql = "SELECT * FROM ( " - " SELECT h.blob_string AS 'blob', h.sequence_number as 'seq', 0 FROM upload_headers h" - " UNION ALL " - " SELECT e.blob_string AS 'blob', e.upload_header as 'seq', 1 FROM events e" - ") " - "ORDER BY 2, 3"; - sqlite3_stmt *selectBlobs; - sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectBlobs, NULL); - NSMutableString *uploadBlobString = [NSMutableString string]; - while (sqlite3_step(selectBlobs) == SQLITE_ROW) { - const char *blob = (const char *)sqlite3_column_text(selectBlobs, 0); - if (blob != NULL) { - NSString *blobString = [[NSString alloc] initWithCString:blob encoding:NSUTF8StringEncoding]; - [uploadBlobString appendString:blobString]; - [blobString release]; - } - } - sqlite3_finalize(selectBlobs); - - return [[uploadBlobString copy] autorelease]; + + // Retrieve the blob strings of each upload header and its child events, in order. + const char *sql = "SELECT * FROM ( " + " SELECT h.blob_string AS 'blob', h.sequence_number as 'seq', 0 FROM upload_headers h" + " UNION ALL " + " SELECT e.blob_string AS 'blob', e.upload_header as 'seq', 1 FROM events e" + ") " + "ORDER BY 2, 3"; + sqlite3_stmt *selectBlobs; + sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectBlobs, NULL); + NSMutableString *uploadBlobString = [NSMutableString string]; + while (sqlite3_step(selectBlobs) == SQLITE_ROW) { + const char *blob = (const char *)sqlite3_column_text(selectBlobs, 0); + if (blob != NULL) { + NSString *blobString = [[NSString alloc] initWithCString:blob encoding:NSUTF8StringEncoding]; + [uploadBlobString appendString:blobString]; + [blobString release]; + } + } + sqlite3_finalize(selectBlobs); + + return [[uploadBlobString copy] autorelease]; } - (BOOL)deleteUploadedData { - // Delete all headers and staged events. - NSString *t = @"delete_upload_data"; - int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE upload_header IS NOT NULL", NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - [self releaseTransaction:t]; - } else { - [self rollbackTransaction:t]; - } - - return code == SQLITE_OK; + // Delete all headers and staged events. + NSString *t = @"delete_upload_data"; + int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE upload_header IS NOT NULL", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + [self releaseTransaction:t]; + } else { + [self rollbackTransaction:t]; + } + + return code == SQLITE_OK; } - (BOOL)resetAnalyticsData { - // Delete or zero all analytics data. - // Reset: headers, events, session number, upload number, last session start, last close event, and last flow event. - // Unaffected: schema version, opt out status, install ID (deprecated), and app key. - - NSString *t = @"reset_analytics_data"; - int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, "DELETE FROM events", NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_rule", NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_ruleevent", NULL, NULL, NULL); - } - - if (code == SQLITE_OK) { - code = sqlite3_exec(_databaseConnection,"UPDATE localytics_info SET last_session_number = 0, last_upload_number = 0," - "last_close_event = null, last_flow_event = null, last_session_start = null, " - "custom_d0 = null, custom_d1 = null, custom_d2 = null, custom_d3 = null, " - "customer_id = null, queued_close_event_blob = null ", - NULL, NULL, NULL); - } - - - if (code == SQLITE_OK) { - [self releaseTransaction:t]; - } else { - [self rollbackTransaction:t]; - } - - return code == SQLITE_OK; + // Delete or zero all analytics data. + // Reset: headers, events, session number, upload number, last session start, last close event, and last flow event. + // Unaffected: schema version, opt out status, install ID (deprecated), and app key. + + NSString *t = @"reset_analytics_data"; + int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR; + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM events", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_rule", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_ruleevent", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_conditions", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_amp_conditions_values", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection, "DELETE FROM localytics_identifiers", NULL, NULL, NULL); + } + + if (code == SQLITE_OK) { + code = sqlite3_exec(_databaseConnection,"UPDATE localytics_info SET last_session_number = 0, last_upload_number = 0," + "last_close_event = null, last_flow_event = null, last_session_start = null, " + "custom_d0 = null, custom_d1 = null, custom_d2 = null, custom_d3 = null, " + "queued_close_event_blob = null, fb_attribution = null", + NULL, NULL, NULL); + } + + + if (code == SQLITE_OK) { + [self releaseTransaction:t]; + } else { + [self rollbackTransaction:t]; + } + + return code == SQLITE_OK; } - (BOOL)vacuumIfRequired { - int code = SQLITE_OK; - if ([self databaseSize] > MAX_DATABASE_SIZE * VACUUM_THRESHOLD) { - code = sqlite3_exec(_databaseConnection, "VACUUM", NULL, NULL, NULL); - } - - return code == SQLITE_OK; + int code = SQLITE_OK; + if ([self databaseSize] > MAX_DATABASE_SIZE * VACUUM_THRESHOLD) { + code = sqlite3_exec(_databaseConnection, "VACUUM", NULL, NULL, NULL); + } + + return code == SQLITE_OK; } - (NSString *)randomUUID { @@ -919,96 +1145,190 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil; return [(NSString *)stringUUID autorelease]; } -- (NSString *)customerId { - NSString *customerId = nil; - - sqlite3_stmt *selectCustomerId; - sqlite3_prepare_v2(_databaseConnection, "SELECT customer_id FROM localytics_info", -1, &selectCustomerId, NULL); - int code = sqlite3_step(selectCustomerId); - if (code == SQLITE_ROW && sqlite3_column_text(selectCustomerId, 0)) { - customerId = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectCustomerId, 0)]; - } - sqlite3_finalize(selectCustomerId); - - return customerId; +- (BOOL)setValueForIdentifier:(NSString *)identifierName value:(NSString *)value +{ + sqlite3_stmt *sqlRule; + if([self valueForIdentifier:identifierName]) { + // Update + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_identifiers SET value=? WHERE key=?", -1, &sqlRule, NULL); + sqlite3_bind_text(sqlRule, 1, [value UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(sqlRule, 2, [identifierName UTF8String], -1, SQLITE_TRANSIENT); + } else { + // Insert + sqlite3_prepare_v2(_databaseConnection, "INSERT INTO localytics_identifiers (value, key) VALUES (?, ?)", -1, &sqlRule, NULL); + sqlite3_bind_text(sqlRule, 1, [value UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(sqlRule, 2, [identifierName UTF8String], -1, SQLITE_TRANSIENT); + } + + int code = sqlite3_step(sqlRule); + sqlite3_finalize(sqlRule); + + return code == SQLITE_DONE; } -- (BOOL)setCustomerId:(NSString *)newCustomerId +- (NSString *)valueForIdentifier:(NSString *)identifierName { - sqlite3_stmt *updateCustomerId; - sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set customer_id = ?", -1, &updateCustomerId, NULL); - sqlite3_bind_text (updateCustomerId, 1, [newCustomerId UTF8String], -1, SQLITE_TRANSIENT); - int code = sqlite3_step(updateCustomerId); - sqlite3_finalize(updateCustomerId); - BOOL success = (code == SQLITE_DONE); - - return success; + NSString *value = nil; + + sqlite3_stmt *selectRuleId; + sqlite3_prepare_v2(_databaseConnection, "SELECT value FROM localytics_identifiers WHERE key = ?", -1, &selectRuleId, NULL); + sqlite3_bind_text(selectRuleId, 1, [identifierName UTF8String], -1, SQLITE_TRANSIENT); + + int code = sqlite3_step(selectRuleId); + if (code == SQLITE_ROW && sqlite3_column_text(selectRuleId, 0)) { + value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectRuleId, 0)]; + } + sqlite3_finalize(selectRuleId); + + return value; +} + +- (BOOL)deleteIdentifer:(NSString *)identifierName +{ + sqlite3_stmt *deleteRule; + sqlite3_prepare_v2(_databaseConnection, "DELETE FROM localytics_identifiers WHERE key = ?", -1, &deleteRule, NULL); + sqlite3_bind_text(deleteRule, 1, [identifierName UTF8String], -1, SQLITE_TRANSIENT); + int code = sqlite3_step(deleteRule); + sqlite3_finalize(deleteRule); + + return (code == SQLITE_DONE);; +} + +- (NSDictionary *)identifiers +{ + NSMutableDictionary *identifiers = nil; + + sqlite3_stmt *selectRule; + sqlite3_prepare_v2(_databaseConnection, "SELECT key, value FROM localytics_identifiers", -1, &selectRule, NULL); + + while (sqlite3_step(selectRule) == SQLITE_ROW) + { + NSString *key = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectRule, 0)]; + NSString *value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectRule, 1)]; + + if(key.length > 0 && value.length > 0) + { + // Defer allocation of the dictionary until we know we need it + // This also allows us to return nil in the event of an empty set + // + if(!identifiers) + { + identifiers = [NSMutableDictionary dictionary]; + } + + [identifiers setObject:value forKey:key]; + } + } + sqlite3_finalize(selectRule); + + return [[identifiers copy] autorelease]; +} + +- (BOOL)setFacebookAttribution:(NSString *)fbAttribution +{ + sqlite3_stmt *updateOptedOut; + sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET fb_attribution = ?", -1, &updateOptedOut, NULL); + sqlite3_bind_text(updateOptedOut, 1, [fbAttribution UTF8String], -1, SQLITE_TRANSIENT); + int code = sqlite3_step(updateOptedOut); + sqlite3_finalize(updateOptedOut); + + return code == SQLITE_OK; +} + +- (void)collectFacebookAttributionIfAvailable +{ + NSString *fbAttribution = [self facebookAttributionFromPasteboard]; + if (fbAttribution) + { + [self setFacebookAttribution:fbAttribution]; + } +} + +- (NSString *)facebookAttributionFromPasteboard +{ + NSString *cookie = nil; + UIPasteboard *pasteBoard = [UIPasteboard + pasteboardWithName:@"fb_app_attribution" + create:NO]; + + if (pasteBoard && [pasteBoard string]) + { + cookie = [pasteBoard string]; + } + + return cookie; +} + +- (NSString *)facebookAttributionFromDb +{ + NSString *facebookAttribution = nil; + + sqlite3_stmt *selectFbAttribution; + sqlite3_prepare_v2(_databaseConnection, "SELECT fb_attribution FROM localytics_info", -1, &selectFbAttribution, NULL); + int code = sqlite3_step(selectFbAttribution); + if (code == SQLITE_ROW) { + char* chars = (char *)sqlite3_column_text(selectFbAttribution, 0); + if(chars) facebookAttribution = [NSString stringWithUTF8String:chars]; + } + sqlite3_finalize(selectFbAttribution); + + return facebookAttribution; } #pragma mark - Safe NSDictionary value methods - (NSInteger)safeIntegerValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key { - NSInteger integerValue = 0; - id value = [dict objectForKey:key]; - if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { - integerValue = [value integerValue]; - } else if ([value isKindOfClass:[NSNull class]]) { - integerValue = 0; - } - - return integerValue; + NSInteger integerValue = 0; + id value = [dict objectForKey:key]; + if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]]) { + integerValue = [value integerValue]; + } else if ([value isKindOfClass:[NSNull class]]) { + integerValue = 0; + } + + return integerValue; } - (NSString *)safeStringValueFromDictionary:(NSDictionary *)dict forKey:(NSString *)key { - NSString *stringValue = nil; - id value = [dict objectForKey:key]; - if ([value isKindOfClass:[NSString class]]) { - stringValue = value; - } else if ([value isKindOfClass:[NSNumber class]]) { - stringValue = [value stringValue]; - } else if ([value isKindOfClass:[NSNull class]]) { - stringValue = nil; - } - - return stringValue; + NSString *stringValue = nil; + id value = [dict objectForKey:key]; + if ([value isKindOfClass:[NSString class]]) { + stringValue = value; + } else if ([value isKindOfClass:[NSNumber class]]) { + stringValue = [value stringValue]; + } else if ([value isKindOfClass:[NSNull class]]) { + stringValue = nil; + } + + return stringValue; } - (NSDictionary *)safeDictionaryFromDictionary:(NSDictionary *)dict forKey:(NSString *)key { - NSDictionary *dictValue = nil; - id value = [dict objectForKey:key]; - if ([value isKindOfClass:[NSDictionary class]]) { - dictValue = value; - } - return dictValue; + NSDictionary *dictValue = nil; + id value = [dict objectForKey:key]; + if ([value isKindOfClass:[NSDictionary class]]) { + dictValue = value; + } + return dictValue; } - (NSArray *)safeListFromDictionary:(NSDictionary *)dict forKey:(NSString *)key { - NSArray *arrayValue = nil; - id value = [dict objectForKey:key]; - if ([value isKindOfClass:[NSArray class]]) { - arrayValue = value; - } - return arrayValue; + NSArray *arrayValue = nil; + id value = [dict objectForKey:key]; + if ([value isKindOfClass:[NSArray class]]) { + arrayValue = value; + } + return arrayValue; } #pragma mark - Lifecycle -+ (id)allocWithZone:(NSZone *)zone { - @synchronized(self) { - if (_sharedLocalyticsDatabase == nil) { - _sharedLocalyticsDatabase = [super allocWithZone:zone]; - return _sharedLocalyticsDatabase; - } - } - // returns nil on subsequent allocations - return nil; -} - - (id)copyWithZone:(NSZone *)zone { +#pragma unused(zone) return self; } @@ -1030,7 +1350,7 @@ static LocalyticsDatabase *_sharedLocalyticsDatabase = nil; } - (void)dealloc { - sqlite3_close(_databaseConnection); + sqlite3_close(_databaseConnection); [super dealloc]; } diff --git a/External/Localytics/LocalyticsSession+Private.h b/External/Localytics/LocalyticsSession+Private.h new file mode 100644 index 00000000..26590c96 --- /dev/null +++ b/External/Localytics/LocalyticsSession+Private.h @@ -0,0 +1,104 @@ +// LocalyticsSession+Private.h +// Copyright (C) 2013 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. + +#import "LocalyticsSession.h" +#import "LocalyticsUploader.h" +#import "LocalyticsDatabase.h" + +#define CLIENT_VERSION_PREFIX @"iOS" +#define LOCALYTICS_LOGGING_ENABLED [[LocalyticsSession shared] loggingEnabled] +#define LocalyticsLog(message, ...)if([[LocalyticsSession shared] loggingEnabled]) \ +[LocalyticsSession logMessage:[NSString stringWithFormat:@"%s:\n + " message "\n\n", __PRETTY_FUNCTION__, ##__VA_ARGS__]] + +@interface LocalyticsSession() +{ + BOOL _hasInitialized; // Whether or not the session object has been initialized. + BOOL _isSessionOpen; // Whether or not this session has been opened. + float _sessionTimeoutInterval; // If an App stays in the background for more + // than this many seconds, start a new session + // when it returns to foreground. +@private +#pragma mark Member Variables + dispatch_queue_t _queue; // Queue of Localytics block objects. + dispatch_group_t _criticalGroup; // Group of blocks the must complete before backgrounding. + NSString *_sessionUUID; // Unique identifier for this session. + NSString *_applicationKey; // Unique identifier for the instrumented application + NSString *_facebookAttribution; // Facebook attribution cookie + NSTimeInterval _lastSessionStartTimestamp; // The start time of the most recent session. + NSDate *_sessionResumeTime; // Time session was started or resumed. + NSDate *_sessionCloseTime; // Time session was closed. + NSMutableString *_unstagedFlowEvents; // Comma-delimited list of app screens and events tagged during this + // session that have NOT been staged for upload. + NSMutableString *_stagedFlowEvents; // App screens and events tagged during this session that HAVE been staged + // for upload. + NSMutableString *_screens; // Comma-delimited list of screens tagged during this session. + NSTimeInterval _sessionActiveDuration; // Duration that session open. + BOOL _sessionHasBeenOpen; // Whether or not this session has ever been open. + BOOL _delaySession; // Whether or not the server should delay processing on this upload + LocalyticsDatabase *_db; // Localytics database reference + LocalyticsUploader *_uploader; // Localytics uploader reference +} + +@property (nonatomic, retain) NSString *applicationKey; +@property (nonatomic, retain) NSString *facebookAttribution; +@property (nonatomic,readonly) dispatch_queue_t queue; +@property (nonatomic,readonly) dispatch_group_t criticalGroup; +@property (atomic) BOOL isSessionOpen; +@property (atomic) BOOL hasInitialized; +@property (nonatomic, retain) NSString *sessionUUID; +@property (nonatomic, assign) NSTimeInterval lastSessionStartTimestamp; +@property (nonatomic, retain) NSDate *sessionResumeTime; +@property (nonatomic, retain) NSDate *sessionCloseTime; +@property (nonatomic, retain) NSMutableString *unstagedFlowEvents; +@property (nonatomic, retain) NSMutableString *stagedFlowEvents; +@property (nonatomic, retain) NSMutableString *screens; +@property (nonatomic, assign) NSTimeInterval sessionActiveDuration; +@property (nonatomic, assign) BOOL sessionHasBeenOpen; +@property (nonatomic, assign) BOOL delaySession; +@property (nonatomic, assign) NSInteger sessionNumber; + +// Private methods. ++ (id)allocFactory; +- (void)reopenPreviousSession; +- (void)addFlowEventWithName:(NSString *)name type:(NSString *)eventType; +- (void)addScreenWithName:(NSString *)name; +- (NSString *)blobHeaderStringWithSequenceNumber:(int)nextSequenceNumber; +- (BOOL)ll_isOptedIn; +- (BOOL)createOptEvent:(BOOL)optState; +- (BOOL)saveApplicationFlowAndRemoveOnResume:(BOOL)removeOnResume; +- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue; +- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue first:(BOOL)firstAttribute; +- (void)uploadCallback:(NSDictionary*)info; ++ (BOOL)appKeyIsValid:(NSString *)appKey; +- (void)ll_open; +- (LocalyticsDatabase *)db; +- (LocalyticsUploader *)uploader; +- (BOOL)uploadIsNeeded; + + +// Datapoint methods. +- (NSString *)customDimensions; +- (NSString *)locationDimensions; +- (NSString *)hashString:(NSString *)input; +- (NSString *)randomUUID; +- (NSString *)escapeString:(NSString *)input; +- (NSString *)installationId; +- (NSString *)appVersion; +- (NSTimeInterval)currentTimestamp; +- (BOOL)isDeviceJailbroken; +- (NSString *)deviceModel; +- (NSString *)modelSizeString; +- (double)availableMemory; +- (NSString *)advertisingIdentifier; +- (NSString *)uniqueDeviceIdentifier; + + + +@end + diff --git a/External/Localytics/LocalyticsSession.h b/External/Localytics/LocalyticsSession.h old mode 100755 new mode 100644 index 9f2f6859..3601f56a --- a/External/Localytics/LocalyticsSession.h +++ b/External/Localytics/LocalyticsSession.h @@ -1,27 +1,28 @@ // LocalyticsSession.h -// Copyright (C) 2012 Char Software Inc., DBA Localytics -// +// Copyright (C) 2013 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. -// +// with this source code. +// // Please visit www.localytics.com for more information. #import #import -// Set this to true to enable localytics traces (useful for debugging) -#define DO_LOCALYTICS_LOGGING false +#define CLIENT_VERSION @"2.17.3" +#define MARKETING_PLATFORM /*! - @class LocalyticsSession + @class LocalyticsSession @discussion The class which manages creating, collecting, & uploading a Localytics session. Please see the following guides for information on how to best use this library, sample code, and other useful information: - + Best Practices
  • Instantiate the LocalyticsSession object in applicationDidFinishLaunching.
  • @@ -33,80 +34,68 @@
  • Do not call any Localytics functions inside a loop. Instead, calls such as tagEvent should follow user actions. This limits the amount of data which is stored and uploaded.
  • -
  • Do not use multiple LocalticsSession objects to upload data with +
  • Do not use multiple LocalticsSession objects to upload data with multiple application keys. This can cause invalid state.
@author Localytics */ -@interface LocalyticsSession : NSObject { +// Forward declaration +@protocol LocalyticsSessionDelegate; - BOOL _hasInitialized; // Whether or not the session object has been initialized. - BOOL _isSessionOpen; // Whether or not this session has been opened. - float _backgroundSessionTimeout; // If an App stays in the background for more - // than this many seconds, start a new session - // when it returns to foreground. - @private - #pragma mark Member Variables - dispatch_queue_t _queue; // Queue of Localytics block objects. - dispatch_group_t _criticalGroup; // Group of blocks the must complete before backgrounding. - NSString *_sessionUUID; // Unique identifier for this session. - NSString *_applicationKey; // Unique identifier for the instrumented application - NSTimeInterval _lastSessionStartTimestamp; // The start time of the most recent session. - NSDate *_sessionResumeTime; // Time session was started or resumed. - NSDate *_sessionCloseTime; // Time session was closed. - NSMutableString *_unstagedFlowEvents; // Comma-delimited list of app screens and events tagged during this - // session that have NOT been staged for upload. - NSMutableString *_stagedFlowEvents; // App screens and events tagged during this session that HAVE been staged - // for upload. - NSMutableString *_screens; // Comma-delimited list of screens tagged during this session. - NSTimeInterval _sessionActiveDuration; // Duration that session open. - BOOL _sessionHasBeenOpen; // Whether or not this session has ever been open. -} +@interface LocalyticsSession : NSObject -@property (nonatomic,readonly) dispatch_queue_t queue; -@property (nonatomic,readonly) dispatch_group_t criticalGroup; -@property BOOL isSessionOpen; -@property BOOL hasInitialized; -@property float backgroundSessionTimeout; - -- (void)logMessage:(NSString *)message; -@property (nonatomic, assign, readonly) NSTimeInterval lastSessionStartTimestamp; -@property (nonatomic, assign, readonly) NSInteger sessionNumber; ++ (void)logMessage:(NSString *)message; /*! @property enableHTTPS - @abstract Determines whether or not HTTPS is used when calling the Localytics + @abstract (Optional) Determines whether or not HTTPS is used when calling the Localytics post URL. The default is NO. */ -@property (nonatomic, assign) BOOL enableHTTPS; // Defaults to NO. +@property (nonatomic, assign) BOOL enableHTTPS; + +/*! + @property loggingEnabled + @abstract (Optional) Determines whether or Localytics debugging information is shown + to the console. The default is NO + */ +@property (nonatomic, assign) BOOL loggingEnabled; + +/*! + @property sessionTimeoutInterval + @abstrac (Optional) If an App stays in the background for more than this many seconds, + start a new session when it returns to foreground. + */ +@property (atomic) float sessionTimeoutInterval; + +/*! + @property localyticsDelegate + @abstract (Optional) Assign this delegate to the class you'd like to register to recieve + the Localytics delegate callbacks (Defined at the end of this file) + */ +@property (nonatomic, assign) id localyticsDelegate; #pragma mark Public Methods /*! - @method sharedLocalyticsSession + @method shared @abstract Accesses the Session object. This is a Singleton class which maintains a single session throughout your application. It is possible to manage your own session, but this is the easiest way to access the Localytics object throughout your code. The class is accessed within the code using the following syntax: - [[LocalyticsSession sharedLocalyticsSession] functionHere] + [[LocalyticsSession shared] functionHere] So, to tag an event, all that is necessary, anywhere in the code is: - [[LocalyticsSession sharedLocalyticsSession] tagEvent:@"MY_EVENT"]; + [[LocalyticsSession shared] tagEvent:@"MY_EVENT"]; */ + (LocalyticsSession *)sharedLocalyticsSession; ++ (LocalyticsSession *)shared; -/*! - @method LocalyticsSession - @abstract Initializes the Localytics Object. Not necessary if you choose to use startSession. - @param applicationKey The key unique for each application generated at www.localytics.com - */ -- (void)LocalyticsSession:(NSString *)appKey; /*! @method startSession - @abstract An optional convenience initialize method that also calls the LocalyticsSession, open & - upload methods. Best Practice is to call open & upload immediately after Localytics Session when loading an app, + @abstract An optional convenience initialize method that also calls the LocalyticsSession, open & + upload methods. Best Practice is to call open & upload immediately after Localytics Session when loading an app, this method fascilitates that behavior. It is recommended that this call be placed in applicationDidFinishLaunching. @param applicationKey The key unique for each application generated @@ -114,31 +103,10 @@ */ - (void)startSession:(NSString *)appKey; -/*! - @method setOptIn - @abstract (OPTIONAL) Allows the application to control whether or not it will collect user data. - Even if this call is used, it is necessary to continue calling upload(). No new data will be - collected, so nothing new will be uploaded but it is necessary to upload an event telling the - server this user has opted out. - @param optedIn True if the user is opted in, false otherwise. - */ -- (void)setOptIn:(BOOL)optedIn; - -/*! - @method isOptedIn - @abstract (OPTIONAL) Whether or not this user has is opted in or out. The only way they can be - opted out is if setOptIn(false) has been called before this. This function should only be - used to pre-populate a checkbox in an options menu. It is not recommended that an application - branch based on Localytics instrumentation because this creates an additional test case. If - the app is opted out, all subsequent Localytics calls will return immediately. - @result true if the user is opted in, false otherwise. - */ -- (BOOL)isOptedIn; - /*! @method open - @abstract Opens the Localytics session. Not necessary if you choose to use startSession. - The session time as presented on the website is the time between open and the + @abstract Opens the Localytics session. Not necessary if you choose to use startSession. + The session time as presented on the website is the time between open and the final close so it is recommended to open the session as early as possible, and close it at the last moment. The session must be opened before any tags can be written. It is recommended that this call be placed in applicationDidFinishLaunching. @@ -150,15 +118,13 @@ /*! @method resume - @abstract Resumes the Localytics session. When the App enters the background, the session is - closed and the time of closing is recorded. When the app returns to the foreground, the session + @abstract Resumes the Localytics session. When the App enters the background, the session is + closed and the time of closing is recorded. When the app returns to the foreground, the session is resumed. If the time since closing is greater than BACKGROUND_SESSION_TIMEOUT, (15 seconds - by default) a new session is created, and uploading is triggered. Otherwise, the previous session + by default) a new session is created, and uploading is triggered. Otherwise, the previous session is reopened. - @return Returns whether the session was resumed or a new session was started. If the user has opted - out of analytics then the return from this method is undefined. -*/ -- (BOOL)resume; + */ +- (void)resume; /*! @method close @@ -176,7 +142,7 @@ @method tagEvent @abstract Allows a session to tag a particular event as having occurred. For example, if a view has three buttons, it might make sense to tag - each button click with the name of the button which was clicked. + each button click with the name of the button which was clicked. For another example, in a game with many levels it might be valuable to create a new tag every time the user gets to a new level in order to determine how far the average user is progressing in the game. @@ -193,36 +159,83 @@
See the tagging guide at: http://wiki.localytics.com/ @param event The name of the event which occurred. + @param attributes (Optional) An object/hash/dictionary of key-value pairs, contains + contextual data specific to the event. + @param rerportAttributes (Optional) Additional attributes used for custom reporting. + Available to Enterprise customers, please contact services for more details. + @param customerValueIncrease (Optional) Numeric value, added to customer lifetime value. + Integer expected. Try to use lowest possible unit, such as cents for US currency. */ - (void)tagEvent:(NSString *)event; -- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes; +- (void)tagEvent:(NSString *)event + attributes:(NSDictionary *)attributes; + +- (void)tagEvent:(NSString *)event + attributes:(NSDictionary *)attributes +customerValueIncrease:(NSNumber *)customerValueIncrease; + +- (void)tagEvent:(NSString *)event + attributes:(NSDictionary *)attributes +reportAttributes:(NSDictionary *)reportAttributes; + +- (void)tagEvent:(NSString *)event + attributes:(NSDictionary *)attributes +reportAttributes:(NSDictionary *)reportAttributes +customerValueIncrease:(NSNumber *)customerValueIncrease; -- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes reportAttributes:(NSDictionary *)reportAttributes; /*! @method tagScreen @abstract Allows tagging the flow of screens encountered during the session. - @param screen The name of the screen + @param screen The name of the screen */ - (void)tagScreen:(NSString *)screen; /*! @method upload - @abstract Creates a low priority thread which uploads any Localytics data already stored - on the device. This should be done early in the process life in order to + @abstract Creates a low priority thread which uploads any Localytics data already stored + on the device. This should be done early in the process life in order to guarantee as much time as possible for slow connections to complete. It is also reasonable to upload again when the application is exiting because if the upload is cancelled the data will just get uploaded the next time the app comes up. */ - (void)upload; +/*! + @method LocalyticsSession + @abstract Initializes the Localytics Object. Not necessary if you choose to use startSession. + @param applicationKey The key unique for each application generated at www.localytics.com + */ +- (void)LocalyticsSession:(NSString *)appKey; + +/*! + @method setOptIn + @abstract (OPTIONAL) Allows the application to control whether or not it will collect user data. + Even if this call is used, it is necessary to continue calling upload(). No new data will be + collected, so nothing new will be uploaded but it is necessary to upload an event telling the + server this user has opted out. + @param optedIn True if the user is opted in, false otherwise. + */ +- (void)setOptIn:(BOOL)optedIn; + +/*! + @method setLocation + @abstract Stores the user's location. This will be used in all event and session calls. + If your application has already collected the user's location, it may be passed to Localytics + via this function. This will cause all events and the session close to include the locatin + information. It is not required that you call this function. + @param deviceLocation The user's location. + */ +- (void)setLocation:(CLLocationCoordinate2D)deviceLocation; + + /*! @method setCustomDimension - @abstract (ENTERPRISE ONLY) Sets the value of a custom dimension. Custom dimensions are dimensions + @abstract Sets the value of a custom dimension. Custom dimensions are dimensions which contain user defined data unlike the predefined dimensions such as carrier, model, and country. Once a value for a custom dimension is set, the device it was set on will continue to upload that value - until the value is changed. To clear a value pass nil as the value. + until the value is changed. To clear a value pass nil as the value. The proper use of custom dimensions involves defining a dimension with less than ten distinct possible values and assigning it to one of the four available custom dimensions. Once assigned this definition should never be changed without changing the App Key otherwise old installs of the application will pollute new data. @@ -230,22 +243,61 @@ - (void)setCustomDimension:(int)dimension value:(NSString *)value; /*! - @method setLocation - @abstract Stores the user's location. This will be used in all event and session calls. - If your application has already collected the user's location, it may be passed to Localytics - via this function. This will cause all events and the session close to include the locatin - information. It is not required that you call this function. - @param deviceLocation The user's location. - */ -- (void)setLocation:(CLLocationCoordinate2D)deviceLocation; + @method customDimension + @abstract Gets the custom dimension value for a given dimension. Avoid calling this on the main thread, as it + may take some time for all pending database execution. */ +- (NSString *)customDimension:(int)dimension; + /*! - @method ampTrigger - @abstract Displays the AMP message for the specific event. - Is a stub implementation here to prevent crashes if this class is accidentally used inplace of - the LocalyticsAmpSession - @param event Name of the event. + @method setValueForIdentifier + @abstract Sets the value of a custom identifier. Identifiers are a form of key/value storage + which contain custom user data. Identifiers might include things like email addresses, customer IDs, twitter + handles, and facebook IDs. + Once a value is set, the device it was set on will continue to upload that value until the value is changed. + To delete a property, pass in nil as the value. */ -- (void)ampTrigger:(NSString *)event; +- (void)setValueForIdentifier:(NSString *)identifierName value:(NSString *)value; + +/*! + @method setCustomerName + @abstract Record the customer name + Once this value is set, the device it was set on will continue to upload that value until the value is changed. + To delete the value, pass in nil. + */ +- (void)setCustomerName:(NSString *)email; + +/*! + @method setCustomerId + @abstract Record your custom customer identifier + Once this value is set, the device it was set on will continue to upload that value until the value is changed. + To delete the value, pass in nil. + */ +- (void)setCustomerId:(NSString *)customerId; + +/*! + @method setCustomerId + @abstract Record the customer's email address + Once this value is set, the device it was set on will continue to upload that value until the value is changed. + To delete the value, pass in nil. + */ +- (void)setCustomerEmail:(NSString *)email; @end + +@protocol LocalyticsSessionDelegate +@optional + +/*! + @method localyticsResumedSession + @abstract Register for this callback to be notified when Localytics has either + resumed a previous session or created a new one. See the on the 'resume' method + for additional details. + @param didResumeExistingSession This flag will indicate if Localytics restored an existing + session or started a new one. + */ +- (void)localyticsResumedSession:(BOOL)didResumeExistingSession; + +@end + + diff --git a/External/Localytics/LocalyticsSession.m b/External/Localytics/LocalyticsSession.m old mode 100755 new mode 100644 index ad660a7c..7d73de6c --- a/External/Localytics/LocalyticsSession.m +++ b/External/Localytics/LocalyticsSession.m @@ -1,13 +1,14 @@ // LocalyticsSession.m -// Copyright (C) 2012 Char Software Inc., DBA Localytics -// +// Copyright (C) 2013 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. -// +// with this source code. +// // Please visit www.localytics.com for more information. #import "LocalyticsSession.h" +#import "LocalyticsSession+Private.h" #import "WebserviceConstants.h" #import "LocalyticsUploader.h" #import "LocalyticsDatabase.h" @@ -22,7 +23,6 @@ #pragma mark Constants #define PREFERENCES_KEY @"_localytics_install_id" // The randomly generated ID for each install of the app -#define CLIENT_VERSION @"iOS_2.14" // The version of this library #define LOCALYTICS_DIR @".localytics" // The directory in which the Localytics database is stored #define IFT_ETHER 0x6 // Ethernet CSMACD #define PATH_TO_APT @"/private/var/lib/apt/" @@ -32,666 +32,775 @@ // The singleton session object. static LocalyticsSession *_sharedLocalyticsSession = nil; -@interface LocalyticsSession() - -@property (nonatomic, retain) NSString *sessionUUID; -@property (nonatomic, retain) NSString *applicationKey; -@property (nonatomic, assign) NSTimeInterval lastSessionStartTimestamp; -@property (nonatomic, retain) NSDate *sessionResumeTime; -@property (nonatomic, retain) NSDate *sessionCloseTime; -@property (nonatomic, retain) NSMutableString *unstagedFlowEvents; -@property (nonatomic, retain) NSMutableString *stagedFlowEvents; -@property (nonatomic, retain) NSMutableString *screens; -@property (nonatomic, assign) NSTimeInterval sessionActiveDuration; -@property (nonatomic, assign) BOOL sessionHasBeenOpen; -@property (nonatomic, assign) NSInteger sessionNumber; - -// Private methods. -- (void)ll_open; -- (void)reopenPreviousSession; -- (void)addFlowEventWithName:(NSString *)name type:(NSString *)eventType; -- (void)addScreenWithName:(NSString *)name; -- (NSString *)blobHeaderStringWithSequenceNumber:(int)nextSequenceNumber; -- (BOOL)ll_isOptedIn; -- (BOOL)createOptEvent:(BOOL)optState; -- (BOOL)saveApplicationFlowAndRemoveOnResume:(BOOL)removeOnResume; -- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue; -- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue first:(BOOL)firstAttribute; --(void) uploadCallback:(NSDictionary*)info; - -// Datapoint methods. -- (NSString *)customDimensions; -- (NSString *)locationDimensions; -- (NSString *)hashString:(NSString *)input; -- (NSString *)randomUUID; -- (NSString *)escapeString:(NSString *)input; -- (NSString *)installationId; -- (NSString *)appVersion; -- (NSTimeInterval)currentTimestamp; -- (BOOL)isDeviceJailbroken; -- (NSString *)deviceModel; -- (NSString *)modelSizeString; -- (double)availableMemory; -- (NSString *)advertisingIdentifier; -- (NSString *)uniqueDeviceIdentifier; - -@end - @implementation LocalyticsSession @synthesize queue = _queue; @synthesize criticalGroup = _criticalGroup; -@synthesize sessionUUID = _sessionUUID; +@synthesize sessionUUID = _sessionUUID; @synthesize applicationKey = _applicationKey; @synthesize lastSessionStartTimestamp = _lastSessionStartTimestamp; @synthesize sessionResumeTime = _sessionResumeTime; @synthesize sessionCloseTime = _sessionCloseTime; @synthesize isSessionOpen = _isSessionOpen; @synthesize hasInitialized = _hasInitialized; -@synthesize backgroundSessionTimeout = _backgroundSessionTimeout; +@synthesize sessionTimeoutInterval = _sessionTimeoutInterval; @synthesize unstagedFlowEvents = _unstagedFlowEvents; @synthesize stagedFlowEvents = _stagedFlowEvents; @synthesize screens = _screens; @synthesize sessionActiveDuration = _sessionActiveDuration; @synthesize sessionHasBeenOpen = _sessionHasBeenOpen; +@synthesize delaySession = _delaySession; @synthesize sessionNumber = _sessionNumber; @synthesize enableHTTPS = _enableHTTPS; +@synthesize loggingEnabled = _loggingEnabled; +@synthesize localyticsDelegate = _localyticsDelegate; +@synthesize facebookAttribution = _facebookAttribution; // Stores the last location passed in to the app. -CLLocationCoordinate2D lastDeviceLocation = {0}; +CLLocationCoordinate2D lastDeviceLocation = {0,0}; #pragma mark Singleton ++ (LocalyticsSession *)sharedLocalyticsSession +{ + return [LocalyticsSession shared]; +} -+ (LocalyticsSession *)sharedLocalyticsSession { ++ (LocalyticsSession *)shared { @synchronized(self) { if (_sharedLocalyticsSession == nil) { - _sharedLocalyticsSession = [[self alloc] init]; + _sharedLocalyticsSession = [[self allocFactory] init]; } } return _sharedLocalyticsSession; } -- (LocalyticsSession *)init { +// Return with retain count of 1 (transfer ownership) ++ (id)allocFactory +{ + return [self alloc]; +} + +- (LocalyticsSession *)init +{ if((self = [super init])) { - _isSessionOpen = NO; - _hasInitialized = NO; - _backgroundSessionTimeout = DEFAULT_BACKGROUND_SESSION_TIMEOUT; - _sessionHasBeenOpen = NO; - _queue = dispatch_queue_create("com.Localytics.operations", DISPATCH_QUEUE_SERIAL); - _criticalGroup = dispatch_group_create(); - _enableHTTPS = NO; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; - - [LocalyticsDatabase sharedLocalyticsDatabase]; - } - - return self; + _loggingEnabled = NO; + _isSessionOpen = NO; + _hasInitialized = NO; + _sessionTimeoutInterval = DEFAULT_BACKGROUND_SESSION_TIMEOUT; + _sessionHasBeenOpen = NO; + _queue = dispatch_queue_create("com.Localytics.operations", DISPATCH_QUEUE_SERIAL); + _criticalGroup = dispatch_group_create(); + _enableHTTPS = NO; + _delaySession = NO; + [_sharedLocalyticsSession db]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + + LocalyticsLog("Localytics core module initialized"); + } + + return self; } #pragma mark Public Methods -- (void)LocalyticsSession:(NSString *)appKey { - // If the session has already initialized, don't bother doing it again. - if(self.hasInitialized) +- (void)LocalyticsSession:(NSString *)appKey +{ + // Take ownership of the appKey, which was directly passed in from the app + // to ensure that it doesn't get released. + [appKey retain]; + + if([LocalyticsSession appKeyIsValid:appKey] == NO) { - [self logMessage:@"Object has already been initialized."]; + [[NSException exceptionWithName:@"Invalid Localytics App Key" + reason:@"The application exception was intentional due to an invalid or incomplete Localytics Application Key. Please verify the Localytics Application Key in the Administration panel and for further details please review the iOS integration guidelines at: http://www.localytics.com/docs/iphone-integration/" + userInfo:nil] raise]; + [appKey release]; return; - } - - @try { - - if(appKey == (id)[NSNull null] || appKey.length == 0) { - [self logMessage:@"App key is null or empty."]; - self.hasInitialized = NO; - return; - } - - // App key should only be alphanumeric chars and dashes. - NSString *trimmedAppKey = [appKey stringByReplacingOccurrencesOfString:@"-" withString:@""]; - if([[trimmedAppKey stringByTrimmingCharactersInSet:[NSCharacterSet alphanumericCharacterSet]] isEqualToString:@""] == false) { - [self logMessage:@"App key may only contain dashes and alphanumeric characters."]; - self.hasInitialized = NO; - return; - } - - if ([LocalyticsDatabase sharedLocalyticsDatabase]) { - // Check if the app key has changed. - NSString *lastAppKey = [[LocalyticsDatabase sharedLocalyticsDatabase] appKey]; - if (![lastAppKey isEqualToString:appKey]) { - if (lastAppKey) { - // Clear previous events and dimensions to guarantee that new data isn't associated with the old app key. - [[LocalyticsDatabase sharedLocalyticsDatabase] resetAnalyticsData]; - - // Vacuum to improve the odds of opening a new session following bulk delete. - [[LocalyticsDatabase sharedLocalyticsDatabase] vacuumIfRequired]; - } - // Record the key for future checks. - [[LocalyticsDatabase sharedLocalyticsDatabase] updateAppKey:appKey]; - } - - self.applicationKey = appKey; - self.hasInitialized = YES; - [self logMessage:[@"Object Initialized. Application's key is: " stringByAppendingString:self.applicationKey]]; - } } - @catch (NSException * e) {} + + dispatch_async(_queue, ^{ + @try { + if ([[LocalyticsSession shared] db]) { + // Check if the app key has changed. + NSString *lastAppKey = [[[LocalyticsSession shared] db] appKey]; + if (![lastAppKey isEqualToString:appKey]) { + if (lastAppKey) { + // Clear previous events and dimensions to guarantee that new data isn't associated with the old app key. + [[[LocalyticsSession shared] db] resetAnalyticsData]; + + // Vacuum to improve the odds of opening a new session following bulk delete. + [[[LocalyticsSession shared] db] vacuumIfRequired]; + } + // Record the key for future checks. + [[[LocalyticsSession shared] db] updateAppKey:appKey]; + } + + self.applicationKey = appKey; + self.hasInitialized = YES; + self.facebookAttribution = [[[LocalyticsSession shared] db] facebookAttributionFromDb]; + self.delaySession = self.facebookAttribution != nil; + + LocalyticsLog("Object Initialized. Application's key is: %@", self.applicationKey); + + if (!NSClassFromString(@"ASIdentifierManager") && ([[[UIDevice currentDevice] systemVersion] floatValue] >= 6.0f)) + { + NSLog(@"\n\n" \ + "**********************************************\n" \ + "- LOCALYTICS WARNING -\n" \ + "AdSupport.framework not detected.\n\n" \ + "This is required to properly track user retention using Apple's 'advertisingIdentifier'.\n" \ + "Please link against this framework and set the reference to 'Optional' to ensure backwards\n" \ + "compatibility with pre-iOS 6 devices. Please reference the iOS integration guide for further information\n" \ + "http://www.localytics.com/docs/iphone-integration/#instruction\n" \ + "**********************************************\n\n"); + } + } + } + @catch (NSException * e) {} + + [appKey release]; + }); } -- (void)startSession:(NSString *)appKey { - //check app key - NSPredicate *matchPred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", @"[A-Fa-f0-9-]+"]; - BOOL matches = [matchPred evaluateWithObject:appKey]; - if (matches == NO) { - //generate exception - NSException *exception = [NSException exceptionWithName:@"Invalid Localytics App Key" reason:@"Application key is not valid. Please look at the iOS integration guidlines at http://www.localytics.com/docs/iphone-integration/" userInfo:nil]; - [exception raise]; - } - +- (void)startSession:(NSString *)appKey +{ + // Create a session [self LocalyticsSession:appKey]; [self open]; [self upload]; } // Public interface to ll_open. -- (void)open { - dispatch_async(_queue, ^{ - [self ll_open]; - }); +- (void)open +{ + dispatch_async(_queue, ^{ + [self ll_open]; + }); } -- (BOOL)resume { - - - // Do nothing if session is already open - if(self.isSessionOpen == YES) - return YES; - - BOOL ret = NO; - // conditions for resuming previous session - if(self.sessionHasBeenOpen && - (!self.sessionCloseTime || - [self.sessionCloseTime timeIntervalSinceNow]*-1 <= self.backgroundSessionTimeout)) { - // Note that we allow the session to be resumed even if the database size exceeds the - // maximum. This is because we don't want to create incomplete sessions. If the DB was large - // enough that the previous session could not be opened, there will be nothing to resume. But - // if this session caused it to go over it is better to let it complete and stop the next one - // from being created. - ret = YES; - dispatch_async(_queue, ^{ - @try { - if([self ll_isOptedIn] == false) { - [self logMessage:@"Can't resume session because user is opted out."]; - } else { - [self logMessage:@"Resume called - Resuming previous session."]; - [self reopenPreviousSession]; - } - } - @catch (NSException * e) {} - }); - - } else { - ret = NO; - dispatch_async(_queue, ^{ - @try { - if([self ll_isOptedIn] == false) { - [self logMessage:@"Can't resume session because user is opted out."]; - } else { - // otherwise open new session and upload - [self logMessage:@"Resume called - Opening a new session."]; - [self ll_open]; - } - } - @catch (NSException * e) {} - }); - - } - self.sessionCloseTime = nil; - return ret; +- (void)resume +{ + dispatch_async(_queue, ^{ + @try { + // Do nothing if session is already open + if(self.isSessionOpen == YES) + { + [self callbackWithDidResume:NO]; + return; + } + + // Do nothing if the user is opted out + if([self ll_isOptedIn] == false) { + LocalyticsLog("Can't resume session because user is opted out."); + [self callbackWithDidResume:NO]; + return; + } + + // Conditions for resuming previous session + if(self.sessionHasBeenOpen && + (!self.sessionCloseTime || + [self.sessionCloseTime timeIntervalSinceNow]*-1 <= self.sessionTimeoutInterval)) { + // Note that we allow the session to be resumed even if the database size exceeds the + // maximum. This is because we don't want to create incomplete sessions. If the DB was large + // enough that the previous session could not be opened, there will be nothing to resume. But + // if this session caused it to go over it is better to let it complete and stop the next one + // from being created. + LocalyticsLog("Resume called - Resuming previous session."); + [self reopenPreviousSession]; + [self callbackWithDidResume:YES]; + } + else { + // Otherwise, open new session and upload + LocalyticsLog("Resume called - Opening a new session."); + [self ll_open]; + [self callbackWithDidResume:NO]; + } + + // Clear stale properties + self.sessionCloseTime = nil; + + } + @catch (NSException * e) {} + }); } -- (void)close { - dispatch_group_async(_criticalGroup, _queue, ^{ - // Do nothing if the session is not open - if (self.isSessionOpen == NO) { - [self logMessage:@"Unable to close session"]; - return; - } - - // Save time of close - self.sessionCloseTime = [NSDate date]; - - // Update active session duration. - self.sessionActiveDuration += [self.sessionCloseTime timeIntervalSinceDate:self.sessionResumeTime]; - - int sessionLength = (int)[[NSDate date] timeIntervalSince1970] - self.lastSessionStartTimestamp; - - @try { - // Create the JSON representing the close blob - NSMutableString *closeEventString = [NSMutableString string]; - [closeEventString appendString:@"{"]; - [closeEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"c" first:YES]]; - [closeEventString appendString:[self formatAttributeWithName:PARAM_SESSION_UUID value:self.sessionUUID]]; - [closeEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]]; - [closeEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp]; - [closeEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_ACTIVE, (long)self.sessionActiveDuration]; - [closeEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; - - // Avoid recording session lengths of users with unreasonable client times (usually caused by developers testing clock change attacks) - if(sessionLength > 0 && sessionLength < 400000) { - [closeEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_TOTAL, sessionLength]; - } - - // Open second level - screen flow - [closeEventString appendFormat:@",\"%@\":[", PARAM_SESSION_SCREENFLOW]; - [closeEventString appendString:self.screens]; - - // Close second level - screen flow - [closeEventString appendString:@"]"]; - - // Append the custom dimensions - [closeEventString appendString:[self customDimensions]]; - - // Append the location - [closeEventString appendString:[self locationDimensions]]; - - // Close first level - close blob - [closeEventString appendString:@"}\n"]; - - BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] queueCloseEventWithBlobString:[[closeEventString copy] autorelease]]; - - self.isSessionOpen = NO; // Session is no longer open. - - if (success) { - [self logMessage:@"Session succesfully closed."]; - } else { - [self logMessage:@"Failed to record session close."]; - } - } - @catch (NSException * e) {} - }); +- (void)callbackWithDidResume:(BOOL)didResumeExistingSession +{ + // Provide optional localyticsResumedSession callback to Localytics delegate + if(self.localyticsDelegate && + [self.localyticsDelegate respondsToSelector:@selector(localyticsResumedSession:)]) + { + // Call them back on the main thread, not the Localytics private queue + dispatch_async(dispatch_get_main_queue(), ^{ + [self.localyticsDelegate localyticsResumedSession:didResumeExistingSession]; + }); + } } -- (void)setOptIn:(BOOL)optedIn { - dispatch_async(_queue, ^{ - @try { - LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase]; - NSString *t = @"set_opt"; - BOOL success = [db beginTransaction:t]; - - // Write out opt event. - if (success) { - success = [self createOptEvent:optedIn]; - } - - // Update database with the option (stored internally as an opt-out). - if (success) { - [db setOptedOut:optedIn == NO]; - } - - if (success && optedIn == NO) { - // Disable all further Localytics calls for this and future sessions - // This should not be flipped when the session is opted back in because that - // would create an incomplete session. - self.isSessionOpen = NO; - } - - if (success) { - [db releaseTransaction:t]; - [self logMessage:[NSString stringWithFormat:@"Application opted %@", optedIn ? @"in" : @"out"]]; - } else { - [db rollbackTransaction:t]; - [self logMessage:@"Failed to update opt state."]; - } - } - @catch (NSException * e) {} - }); +- (void)close +{ + dispatch_group_async(_criticalGroup, _queue, ^{ + @try { + + // Do nothing if the session is not open + if (self.isSessionOpen == NO) { + LocalyticsLog("Unable to close session"); + return; + } + + // Save time of close + self.sessionCloseTime = [NSDate date]; + + // Update active session duration. + self.sessionActiveDuration += [self.sessionCloseTime timeIntervalSinceDate:self.sessionResumeTime]; + + int sessionLength = (int)[[NSDate date] timeIntervalSince1970] - self.lastSessionStartTimestamp; + + + // Create the JSON representing the close blob + NSMutableString *closeEventString = [NSMutableString string]; + [closeEventString appendString:@"{"]; + [closeEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"c" first:YES]]; + [closeEventString appendString:[self formatAttributeWithName:PARAM_SESSION_UUID value:self.sessionUUID]]; + [closeEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]]; + [closeEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp]; + [closeEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_ACTIVE, (long)self.sessionActiveDuration]; + [closeEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; + + // Avoid recording session lengths of users with unreasonable client times (usually caused by developers testing clock change attacks) + if(sessionLength > 0 && sessionLength < 400000) { + [closeEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_TOTAL, sessionLength]; + } + + // Open second level - screen flow + [closeEventString appendFormat:@",\"%@\":[", PARAM_SESSION_SCREENFLOW]; + [closeEventString appendString:self.screens]; + + // Close second level - screen flow + [closeEventString appendString:@"]"]; + + // Append the custom dimensions + [closeEventString appendString:[self customDimensions]]; + + // Append the location + [closeEventString appendString:[self locationDimensions]]; + + // Close first level - close blob + [closeEventString appendString:@"}\n"]; + + BOOL success = [[self db] queueCloseEventWithBlobString:[[closeEventString copy] autorelease]]; + + self.isSessionOpen = NO; // Session is no longer open. + + if (success) { + LocalyticsLog("Session succesfully closed."); + } + else { + LocalyticsLog("Failed to record session close."); + } + } + @catch (NSException * e) {} + }); } -// Public interface to ll_isOptedIn. -- (BOOL)isOptedIn { - __block BOOL optedIn = YES; - dispatch_sync(_queue, ^{ - optedIn = [self ll_isOptedIn]; - }); - return optedIn; +- (void)setOptIn:(BOOL)optedIn +{ + dispatch_async(_queue, ^{ + @try { + LocalyticsDatabase *db = [self db]; + NSString *t = @"set_opt"; + BOOL success = [db beginTransaction:t]; + + // Write out opt event. + if (success) { + success = [self createOptEvent:optedIn]; + } + + // Update database with the option (stored internally as an opt-out). + if (success) { + [db setOptedOut:optedIn == NO]; + } + + if (success && optedIn == NO) { + // Disable all further Localytics calls for this and future sessions + // This should not be flipped when the session is opted back in because that + // would create an incomplete session. + self.isSessionOpen = NO; + } + + if (success) { + [db releaseTransaction:t]; + LocalyticsLog("Application opted %@", optedIn ? @"in" : @"out"); + } else { + [db rollbackTransaction:t]; + LocalyticsLog("Failed to update opt state."); + } + } + @catch (NSException * e) {} + }); } // A convenience function for users who don't wish to add attributes. -- (void)tagEvent:(NSString *)event { +- (void)tagEvent:(NSString *)event +{ [self tagEvent:event attributes:nil reportAttributes:nil]; } // Most users should use this tagEvent call. -- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes { +- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes +{ [self tagEvent:event attributes:attributes reportAttributes:nil]; } -- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes reportAttributes:(NSDictionary *)reportAttributes { - dispatch_async(_queue, ^{ - @try { - // Do nothing if the session is not open. - if (self.isSessionOpen == NO) - { - [self logMessage:@"Cannot tag an event because the session is not open."]; - return; - } - - if(event == (id)[NSNull null] || event.length == 0) - { - [self logMessage:@"Event tagged without a name. Skipping."]; - return; - } - - // Create the JSON for the event - NSMutableString *eventString = [[[NSMutableString alloc] init] autorelease]; - [eventString appendString:@"{"]; - [eventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"e" first:YES] ]; - [eventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]]; - [eventString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey ]]; - [eventString appendString:[self formatAttributeWithName:PARAM_SESSION_UUID value:self.sessionUUID ]]; - [eventString appendString:[self formatAttributeWithName:PARAM_EVENT_NAME value:[self escapeString:event] ]]; - [eventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; - - // Append the custom dimensions - [eventString appendString:[self customDimensions]]; - - // Append the location - [eventString appendString:[self locationDimensions]]; - - // If there are any attributes for this event, add them as a hash - int attrIndex = 0; - if(attributes != nil) - { - // Open second level - attributes - [eventString appendString:[NSString stringWithFormat:@",\"%@\":{", PARAM_ATTRIBUTES]]; - for (id key in [attributes allKeys]) - { - // Have to escape paramName and paramValue because they user-defined. - [eventString appendString: - [self formatAttributeWithName:[self escapeString:[key description]] - value:[self escapeString:[[attributes valueForKey:key] description]] - first:(attrIndex == 0)]]; - attrIndex++; - } - - // Close second level - attributes - [eventString appendString:@"}"]; - } - - // If there are any report attributes for this event, add them as above - attrIndex = 0; - if(reportAttributes != nil) - { - [eventString appendString:[NSString stringWithFormat:@",\"%@\":{", PARAM_REPORT_ATTRIBUTES]]; - for(id key in [reportAttributes allKeys]) { - [eventString appendString: - [self formatAttributeWithName:[self escapeString:[key description]] - value:[self escapeString:[[reportAttributes valueForKey:key] description]] - first:(attrIndex == 0)]]; - attrIndex++; - } - [eventString appendString:@"}"]; - } - - // Close first level - Event information - [eventString appendString:@"}\n"]; - - BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] addEventWithBlobString:[[eventString copy] autorelease]]; - if (success) { - // User-originated events should be tracked as application flow. - [self addFlowEventWithName:event type:@"e"]; // "e" for Event. - - [self ampTrigger:event]; - - [self logMessage:[@"Tagged event: " stringByAppendingString:event]]; - } else { - [self logMessage:@"Failed to tag event."]; - } - } - @catch (NSException * e) {} - }); +- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes customerValueIncrease:(NSNumber *)value +{ + [self tagEvent:event attributes:attributes reportAttributes:nil customerValueIncrease:value]; } -- (void)tagScreen:(NSString *)screen { - dispatch_async(_queue, ^{ - // Do nothing if the session is not open. - if (self.isSessionOpen == NO) - { - [self logMessage:@"Cannot tag a screen because the session is not open."]; - return; - } - - // Tag screen with description to enforce string type and avoid retaining objects passed by clients in lieu of a - // screen name. - NSString *screenName = [screen description]; - [self addFlowEventWithName:screenName type:@"s"]; // "s" for Screen. - - // Maintain a parallel list of only screen names. This is submitted in the session close event. - // This may be removed in a future version of the client library. - [self addScreenWithName:screenName]; - - [self logMessage:[@"Tagged screen: " stringByAppendingString:screenName]]; - }); +- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes reportAttributes:(NSDictionary *)reportAttributes +{ + [self tagEvent:event attributes:attributes reportAttributes:reportAttributes customerValueIncrease:nil]; } -- (void)setLocation:(CLLocationCoordinate2D)deviceLocation { - lastDeviceLocation = deviceLocation; - [self logMessage:@"Setting Location"]; +- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes reportAttributes:(NSDictionary *)reportAttributes customerValueIncrease:(NSNumber *)value +{ + dispatch_async(_queue, ^{ + @try { + // Do nothing if the session is not open. + if (self.isSessionOpen == NO) + { + LocalyticsLog("Cannot tag an event because the session is not open."); + return; + } + + if(event == (id)[NSNull null] || event.length == 0) + { + LocalyticsLog("Event tagged without a name. Skipping."); + return; + } + + + // Create the JSON for the event + NSMutableString *eventString = [[[NSMutableString alloc] init] autorelease]; + [eventString appendString:@"{"]; + + [eventString appendString: + [self formatAttributeWithName:PARAM_DATA_TYPE + value:@"e" first:YES] ]; + [eventString appendString: + [self formatAttributeWithName:PARAM_UUID + value:[self randomUUID] ]]; + [eventString appendString: + [self formatAttributeWithName:PARAM_APP_KEY + value:self.applicationKey ]]; + [eventString appendString: + [self formatAttributeWithName:PARAM_SESSION_UUID + value:self.sessionUUID ]]; + [eventString appendString: + [self formatAttributeWithName:PARAM_EVENT_NAME + value:[self escapeString:event] ]]; + + if(value) + { + [eventString appendString: + [self formatAttributeWithName:PARAM_VALUE_NAME + value:[value stringValue] ]]; + } + + [eventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; + + + // Append the custom dimensions + [eventString appendString:[self customDimensions]]; + + + // Append the location + [eventString appendString:[self locationDimensions]]; + + + // If there are any attributes for this event, add them as a hash + int attrIndex = 0; + if(attributes != nil) + { + // Open second level - attributes + [eventString appendString:[NSString stringWithFormat:@",\"%@\":{", PARAM_ATTRIBUTES]]; + for (id key in [attributes allKeys]) + { + // Have to escape paramName and paramValue because they user-defined. + [eventString appendString: + [self formatAttributeWithName:[self escapeString:[key description]] + value:[self escapeString:[[attributes valueForKey:key] description]] + first:(attrIndex == 0)]]; + attrIndex++; + } + + // Close second level - attributes + [eventString appendString:@"}"]; + } + + + // If there are any report attributes for this event, add them as above + attrIndex = 0; + if(reportAttributes != nil) + { + [eventString appendString:[NSString stringWithFormat:@",\"%@\":{", PARAM_REPORT_ATTRIBUTES]]; + for(id key in [reportAttributes allKeys]) { + [eventString appendString: + [self formatAttributeWithName:[self escapeString:[key description]] + value:[self escapeString:[[reportAttributes valueForKey:key] description]] + first:(attrIndex == 0)]]; + attrIndex++; + } + [eventString appendString:@"}"]; + } + + + // Close first level - Event information + [eventString appendString:@"}\n"]; + + BOOL success = [[self db] addEventWithBlobString:[[eventString copy] autorelease]]; + if (success) { + // User-originated events should be tracked as application flow. + [self addFlowEventWithName:event type:@"e"]; // "e" for Event. + + LocalyticsLog("Tagged event: %@", event); + } + else { + LocalyticsLog("Failed to tag event."); + } + } + @catch (NSException * e) {} + }); } -- (void)setCustomDimension:(int)dimension value:(NSString *)value { - dispatch_async(_queue, ^{ - if(dimension < 0 || dimension > 3) { - [self logMessage:@"Only valid dimensions are 0 - 3"]; - return; - } - - if(false == [[LocalyticsDatabase sharedLocalyticsDatabase] setCustomDimension:dimension value:value]) { - [self logMessage:@"Unable to set custom dimensions."]; - } - }); +- (void)tagScreen:(NSString *)screen +{ + dispatch_async(_queue, ^{ + // Do nothing if the session is not open. + if (self.isSessionOpen == NO) + { + LocalyticsLog("Cannot tag a screen because the session is not open."); + return; + } + + // Tag screen with description to enforce string type and avoid retaining objects passed by clients in lieu of a + // screen name. + NSString *screenName = [screen description]; + [self addFlowEventWithName:screenName type:@"s"]; // "s" for Screen. + + // Maintain a parallel list of only screen names. This is submitted in the session close event. + // This may be removed in a future version of the client library. + [self addScreenWithName:screenName]; + + LocalyticsLog("Tagged screen: %@", screenName);; + }); } -- (void)upload { - dispatch_group_async(_criticalGroup, _queue, ^{ - @try { - if ([[LocalyticsUploader sharedLocalyticsUploader] isUploading]) { - [self logMessage:@"An upload is already in progress. Aborting."]; - return; - } +- (void)setLocation:(CLLocationCoordinate2D)deviceLocation +{ + lastDeviceLocation = deviceLocation; + LocalyticsLog("Setting Location"); +} - NSString *t = @"stage_upload"; - LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase]; - BOOL success = [db beginTransaction:t]; +- (void)setCustomDimension:(int)dimension value:(NSString *)value +{ + if(dimension < 0 || dimension > 9) { + LocalyticsLog("Only valid dimensions are 0 - 9"); + return; + } + + dispatch_async(_queue, ^{ + if(![[self db] setCustomDimension:dimension value:value]) { + LocalyticsLog("Unable to set custom dimensions."); + } + }); +} - // - The event list for the current session is not modified - // New flow events are only transitioned to the "old" list if the upload is staged successfully. The queue - // ensures that the list of events are not modified while a call to upload is in progress. - if (success) { - // Write flow blob to database. This is for a session in progress and should not be removed upon resume. - success = [self saveApplicationFlowAndRemoveOnResume:NO]; - } +- (void)setValueForIdentifier:(NSString *)identifierName value:(NSString *)value +{ + if(identifierName.length == 0) { + LocalyticsLog("Cannot set user identifier. Empty key value"); + return; + } + + // If the identifier value is nil, then delete the entry from the db + if(value.length == 0) { + dispatch_async(_queue, ^{ + @try { + if(![[self db] deleteIdentifer:identifierName]) { + LocalyticsLog("Failed to delete identifier with key %@", identifierName); + } + } + @catch (NSException *e) {} + }); + } + + // Otherwise update or insert the key/value pair into the db + else { + dispatch_async(_queue, ^{ + @try { + if(![[self db] setValueForIdentifier:identifierName value:value]) { + LocalyticsLog("Unable to set user identifier %@ with value %@", identifierName, value); + } + } + @catch (NSException *e) {} + }); + } + +} - if (success && [db unstagedEventCount] > 0) { - // Increment upload sequence number. - int sequenceNumber = 0; - success = [db incrementLastUploadNumber:&sequenceNumber]; - - // Write out header to database. - sqlite3_int64 headerRowId = 0; - if (success) { - NSString *headerBlob = [self blobHeaderStringWithSequenceNumber:sequenceNumber]; - success = [db addHeaderWithSequenceNumber:sequenceNumber blobString:headerBlob rowId:&headerRowId]; - } +- (void)setCustomerName:(NSString *)customerName +{ + [self setValueForIdentifier:@"customer_name" value:customerName]; +} - // Associate unstaged events. - if (success) { - success = [db stageEventsForUpload:headerRowId]; - } - } - - if (success) { - // Complete transaction - [db releaseTransaction:t]; +- (void)setCustomerId:(NSString *)customerId +{ + [self setValueForIdentifier:@"customer_id" value:customerId]; +} - // Move new flow events to the old flow event array. - if (self.unstagedFlowEvents.length) { - if (self.stagedFlowEvents.length) { - [self.stagedFlowEvents appendFormat:@",%@", self.unstagedFlowEvents]; - } else { - self.stagedFlowEvents = [[self.unstagedFlowEvents mutableCopy] autorelease]; - } - self.unstagedFlowEvents = [NSMutableString string]; - } - - // Begin upload. - [[LocalyticsUploader sharedLocalyticsUploader] uploaderWithApplicationKey:self.applicationKey useHTTPS:[self enableHTTPS] installId:[self installationId] resultTarget:self callback:@selector(uploadCallback:)]; - } else { - [db rollbackTransaction:t]; - [self logMessage:@"Failed to start upload."]; - } - } - @catch (NSException * e) { } - }); +- (void)setCustomerEmail:(NSString *)email +{ + [self setValueForIdentifier:@"email" value:email]; +} + +- (void)upload +{ + dispatch_group_async(_criticalGroup, _queue, ^{ + @try { + if ([[self uploader] isUploading]) { + LocalyticsLog("An upload is already in progress. Aborting."); + return; + } + + NSString *t = @"stage_upload"; + LocalyticsDatabase *db = [self db]; + BOOL success = [db beginTransaction:t]; + + // - The event list for the current session is not modified + // New flow events are only transitioned to the "old" list if the upload is staged successfully. The queue + // ensures that the list of events are not modified while a call to upload is in progress. + if (success) { + // Write flow blob to database. This is for a session in progress and should not be removed upon resume. + success = [self saveApplicationFlowAndRemoveOnResume:NO]; + } + + if (success && [self uploadIsNeeded]) + { + // Increment upload sequence number. + int sequenceNumber = 0; + success = [db incrementLastUploadNumber:&sequenceNumber]; + + // Write out header to database. + sqlite3_int64 headerRowId = 0; + if (success) { + NSString *headerBlob = [self blobHeaderStringWithSequenceNumber:sequenceNumber]; + success = [db addHeaderWithSequenceNumber:sequenceNumber blobString:headerBlob rowId:&headerRowId]; + } + + // Associate unstaged events. + if (success) { + success = [db stageEventsForUpload:headerRowId]; + } + } + + if (success) { + // Complete transaction + [db releaseTransaction:t]; + + // Move new flow events to the old flow event array. + if (self.unstagedFlowEvents.length) { + if (self.stagedFlowEvents.length) { + [self.stagedFlowEvents appendFormat:@",%@", self.unstagedFlowEvents]; + } else { + self.stagedFlowEvents = [[self.unstagedFlowEvents mutableCopy] autorelease]; + } + self.unstagedFlowEvents = [NSMutableString string]; + } + + // Begin upload. + [[self uploader] uploaderWithApplicationKey:self.applicationKey + useHTTPS:[self enableHTTPS] + installId:[self installationId] + libraryVersion:[self libraryVersion] + resultTarget:self + callback:@selector(uploadCallback:)]; + } + else { + [db rollbackTransaction:t]; + LocalyticsLog("Failed to start upload."); + } + } + @catch (NSException * e) { } + }); + + [self uploadPartnerAttributions]; +} + +- (BOOL)uploadIsNeeded +{ + return [[self db] unstagedEventCount] > 0; +} + +- (void)uploadPartnerAttributions +{ + dispatch_group_async(_criticalGroup, _queue, ^{ + @try { + if (!self.facebookAttribution) + return; + + [[self uploader] uploaderAttributionWithApplicationKey:self.applicationKey + attribution:self.facebookAttribution + installId:[self installationId] + advertisingIdentifier:[self advertisingIdentifier]]; + } + @catch (NSException *e) {} + }); } #pragma mark Private Methods --(NSString*)libraryVersion { - return CLIENT_VERSION; +- (NSString*)libraryVersion +{ + return [NSString stringWithFormat:@"%@_%@",CLIENT_VERSION_PREFIX, CLIENT_VERSION]; } --(void) uploadCallback:(NSDictionary*)info{ +- (void)uploadCallback:(NSDictionary*)info +{ +#pragma unused(info) } - (void)dequeueCloseEventBlobString { - LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase]; - NSString *closeEventString = [db dequeueCloseEventBlobString]; - if (closeEventString) { - BOOL success = [db addCloseEventWithBlobString:closeEventString]; - if (!success) { - // Re-queue the close event. - [db queueCloseEventWithBlobString:closeEventString]; - } - } + LocalyticsDatabase *db = [self db]; + NSString *closeEventString = [db dequeueCloseEventBlobString]; + if (closeEventString) { + BOOL success = [db addCloseEventWithBlobString:closeEventString]; + if (!success) { + // Re-queue the close event. + [db queueCloseEventWithBlobString:closeEventString]; + } + } } -- (void)ll_open { - // There are a number of conditions in which nothing should be done: - if (self.hasInitialized == NO || // the session object has not yet initialized - self.isSessionOpen == YES) // session has already been opened - { - [self logMessage:@"Unable to open session."]; - return; - } - - if([self ll_isOptedIn] == false) { - [self logMessage:@"Can't open session because user is opted out."]; - return; - } - - @try { - // If there is too much data on the disk, don't bother collecting any more. - LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase]; - if([db databaseSize] > MAX_DATABASE_SIZE) { - [self logMessage:@"Database has exceeded the maximum size. Session not opened."]; - self.isSessionOpen = NO; - return; - } - - [self dequeueCloseEventBlobString]; - - self.sessionActiveDuration = 0; - self.sessionResumeTime = [NSDate date]; - self.unstagedFlowEvents = [NSMutableString string]; - self.stagedFlowEvents = [NSMutableString string]; - self.screens = [NSMutableString string]; - - // Begin transaction for session open. - NSString *t = @"open_session"; - BOOL success = [db beginTransaction:t]; - - // lastSessionStartTimestamp isn't really the last session start time. - // It's the sessionResumeTime which is [NSDate date] or now. Therefore, - // save the current lastSessionTimestamp value from the database so it - // can be used to calculate the elapsed time between session start times. - NSTimeInterval previousSessionStartTimeInterval = [db lastSessionStartTimestamp]; - - // Save session start time. - self.lastSessionStartTimestamp = [self.sessionResumeTime timeIntervalSince1970]; - if (success) { - success = [db setLastsessionStartTimestamp:self.lastSessionStartTimestamp]; - } - - // Retrieve next session number. - int sessionNumber = 0; - if (success) { - success = [db incrementLastSessionNumber:&sessionNumber]; - } - [self setSessionNumber:sessionNumber]; - - if (success) { - // Prepare session open event. - self.sessionUUID = [self randomUUID]; - - // Store event. - NSMutableString *openEventString = [NSMutableString string]; - [openEventString appendString:@"{"]; - [openEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"s" first:YES]]; - [openEventString appendString:[self formatAttributeWithName:PARAM_NEW_SESSION_UUID value:self.sessionUUID]]; - [openEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)self.lastSessionStartTimestamp]; - [openEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_NUMBER, sessionNumber]; - - double elapsedTime = 0.0; - if (previousSessionStartTimeInterval > 0) { - elapsedTime = [self lastSessionStartTimestamp] - previousSessionStartTimeInterval; - } - NSString *elapsedTimeString = [NSString stringWithFormat:@"%.0f", elapsedTime]; - [openEventString appendString:[self formatAttributeWithName:PARAM_SESSION_ELAPSE_TIME value:elapsedTimeString]]; - - [openEventString appendString:[self customDimensions]]; - [openEventString appendString:[self locationDimensions]]; - - [openEventString appendString:@"}\n"]; - - [self customDimensions]; - - success = [db addEventWithBlobString:[[openEventString copy] autorelease]]; - } - - if (success) { - [db releaseTransaction:t]; - self.isSessionOpen = YES; - self.sessionHasBeenOpen = YES; - [self logMessage:[@"Succesfully opened session. UUID is: " stringByAppendingString:self.sessionUUID]]; - } else { - [db rollbackTransaction:t]; - self.isSessionOpen = NO; - [self logMessage:@"Failed to open session."]; - } - } - @catch (NSException * e) {} +- (void)ll_open +{ + @try { + // There are a number of conditions in which nothing should be done: + if (self.hasInitialized == NO || // the session object has not yet initialized + self.isSessionOpen == YES) // session has already been opened + { + LocalyticsLog("Unable to open session."); + return; + } + + if([self ll_isOptedIn] == false) { + LocalyticsLog("Can't open session because user is opted out."); + return; + } + + // If there is too much data on the disk, don't bother collecting any more. + LocalyticsDatabase *db = [self db]; + if([db databaseSize] > MAX_DATABASE_SIZE) { + LocalyticsLog("Database has exceeded the maximum size. Session not opened."); + self.isSessionOpen = NO; + return; + } + + [self dequeueCloseEventBlobString]; + + self.sessionActiveDuration = 0; + self.sessionResumeTime = [NSDate date]; + self.unstagedFlowEvents = [NSMutableString string]; + self.stagedFlowEvents = [NSMutableString string]; + self.screens = [NSMutableString string]; + + // Begin transaction for session open. + NSString *t = @"open_session"; + BOOL success = [db beginTransaction:t]; + + // lastSessionStartTimestamp isn't really the last session start time. + // It's the sessionResumeTime which is [NSDate date] or now. Therefore, + // save the current lastSessionTimestamp value from the database so it + // can be used to calculate the elapsed time between session start times. + NSTimeInterval previousSessionStartTimeInterval = [db lastSessionStartTimestamp]; + + // Save session start time. + self.lastSessionStartTimestamp = [self.sessionResumeTime timeIntervalSince1970]; + if (success) { + success = [db setLastSessionStartTimestamp:self.lastSessionStartTimestamp]; + } + + // Retrieve next session number. + int sessionNumber = 0; + if (success) { + success = [db incrementLastSessionNumber:&sessionNumber]; + } + [self setSessionNumber:sessionNumber]; + + if (success) { + // Prepare session open event. + self.sessionUUID = [self randomUUID]; + + // Store event. + NSMutableString *openEventString = [NSMutableString string]; + [openEventString appendString:@"{"]; + [openEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"s" first:YES]]; + [openEventString appendString:[self formatAttributeWithName:PARAM_NEW_SESSION_UUID value:self.sessionUUID]]; + [openEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)self.lastSessionStartTimestamp]; + [openEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_NUMBER, sessionNumber]; + + double elapsedTime = 0.0; + if (previousSessionStartTimeInterval > 0) { + elapsedTime = [self lastSessionStartTimestamp] - previousSessionStartTimeInterval; + } + NSString *elapsedTimeString = [NSString stringWithFormat:@"%.0f", elapsedTime]; + [openEventString appendString:[self formatAttributeWithName:PARAM_SESSION_ELAPSE_TIME value:elapsedTimeString]]; + + [openEventString appendString:[self customDimensions]]; + [openEventString appendString:[self locationDimensions]]; + + [openEventString appendString:@"}\n"]; + + [self customDimensions]; + + success = [db addEventWithBlobString:[[openEventString copy] autorelease]]; + } + + if (success) { + [db releaseTransaction:t]; + self.isSessionOpen = YES; + self.sessionHasBeenOpen = YES; + LocalyticsLog("Succesfully opened session. UUID is: %@", self.sessionUUID); + } + else { + [db rollbackTransaction:t]; + self.isSessionOpen = NO; + LocalyticsLog("Failed to open session."); + } + } + @catch (NSException * e) {} } /*! @method reopenPreviousSession @abstract Reopens the previous session, using previous session variables. If there was no previous session, do nothing. -*/ -- (void)reopenPreviousSession { - if(self.sessionHasBeenOpen == NO){ - [self logMessage:@"Unable to reopen previous session, because a previous session was never opened."]; - return; - } - - // Record session resume time. - self.sessionResumeTime = [NSDate date]; - - //Remove close and flow events if they exist. - [[LocalyticsDatabase sharedLocalyticsDatabase] removeLastCloseAndFlowEvents]; - - self.isSessionOpen = YES; + */ +- (void)reopenPreviousSession +{ + if(self.sessionHasBeenOpen == NO){ + LocalyticsLog("Unable to reopen previous session, because a previous session was never opened."); + return; + } + + // Record session resume time. + self.sessionResumeTime = [NSDate date]; + + //Remove close and flow events if they exist. + [[self db] removeLastCloseAndFlowEvents]; + + self.isSessionOpen = YES; } /*! @@ -700,19 +809,20 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @param name The name of the tagged event. @param eventType A key representing the type of the tagged event. Either "s" for Screen or "e" for Event. */ -- (void)addFlowEventWithName:(NSString *)name type:(NSString *)eventType { - if (!name || !eventType) - return; - - // Format new event as simple key-value dictionary. - NSString *eventString = [self formatAttributeWithName:eventType value:[self escapeString:name] first:YES]; - - // Flow events are uploaded as a sequence of key-value pairs. Wrap the above in braces and append to the list. - BOOL previousFlowEvents = self.unstagedFlowEvents.length > 0; - if (previousFlowEvents) { - [self.unstagedFlowEvents appendString:@","]; - } - [self.unstagedFlowEvents appendFormat:@"{%@}", eventString]; +- (void)addFlowEventWithName:(NSString *)name type:(NSString *)eventType +{ + if (!name || !eventType) + return; + + // Format new event as simple key-value dictionary. + NSString *eventString = [self formatAttributeWithName:eventType value:[self escapeString:name] first:YES]; + + // Flow events are uploaded as a sequence of key-value pairs. Wrap the above in braces and append to the list. + BOOL previousFlowEvents = self.unstagedFlowEvents.length > 0; + if (previousFlowEvents) { + [self.unstagedFlowEvents appendString:@","]; + } + [self.unstagedFlowEvents appendFormat:@"{%@}", eventString]; } /*! @@ -722,11 +832,12 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; screen flow events list and may be removed in future versions of this library. @param name The name of the tagged screen. */ -- (void)addScreenWithName:(NSString *)name { - if (self.screens.length > 0) { - [self.screens appendString:@","]; - } - [self.screens appendFormat:@"\"%@\"", [self escapeString:name]]; +- (void)addScreenWithName:(NSString *)name +{ + if (self.screens.length > 0) { + [self.screens appendString:@","]; + } + [self.screens appendFormat:@"\"%@\"", [self escapeString:name]]; } /*! @@ -735,68 +846,93 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @param nextSequenceNumber The sequence number for the current upload attempt. @return The upload header JSON blob. */ -- (NSString *)blobHeaderStringWithSequenceNumber:(int)nextSequenceNumber { - - NSMutableString *headerString = [[[NSMutableString alloc] init] autorelease]; - - // Common header information. +- (NSString *)blobHeaderStringWithSequenceNumber:(int)nextSequenceNumber +{ + NSMutableString *headerString = [[[NSMutableString alloc] init] autorelease]; + + // Common header information. + // UIDevice *thisDevice = [UIDevice currentDevice]; NSLocale *locale = [NSLocale currentLocale]; NSLocale *english = [[[NSLocale alloc] initWithLocaleIdentifier: @"en_US"] autorelease]; - NSLocale *device_locale = [[NSLocale preferredLanguages] objectAtIndex:0]; - NSString *device_language = [english displayNameForKey:NSLocaleIdentifier value:device_locale]; + NSLocale *device_locale = [[NSLocale preferredLanguages] objectAtIndex:0]; + NSString *device_language = [english displayNameForKey:NSLocaleIdentifier value:device_locale]; NSString *locale_country = [english displayNameForKey:NSLocaleCountryCode value:[locale objectForKey:NSLocaleCountryCode]]; - NSString *uuid = [self randomUUID]; + NSString *uuid = [self randomUUID]; NSString *device_uuid = [self uniqueDeviceIdentifier]; - NSString *device_adid = [self advertisingIdentifier]; - - // Open first level - blob information - [headerString appendString:@"{"]; - [headerString appendFormat:@"\"%@\":%d", PARAM_SEQUENCE_NUMBER, nextSequenceNumber]; - [headerString appendFormat:@",\"%@\":%ld", PARAM_PERSISTED_AT, (long)[[LocalyticsDatabase sharedLocalyticsDatabase] createdTimestamp]]; - [headerString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"h" ]]; - [headerString appendString:[self formatAttributeWithName:PARAM_UUID value:uuid ]]; - - // Open second level - blob header attributes - [headerString appendString:[NSString stringWithFormat:@",\"%@\":{", PARAM_ATTRIBUTES]]; - [headerString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"a" first:YES]]; - + NSString *device_adid = [self advertisingIdentifier]; + + // Open first level - blob information + [headerString appendString:@"{"]; + [headerString appendFormat:@"\"%@\":%d", PARAM_SEQUENCE_NUMBER, nextSequenceNumber]; + [headerString appendFormat:@",\"%@\":%ld", PARAM_PERSISTED_AT, (long)[[self db] createdTimestamp]]; + [headerString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"h" ]]; + [headerString appendString:[self formatAttributeWithName:PARAM_UUID value:uuid ]]; + + // Open second level - blob header attributes + [headerString appendString:[NSString stringWithFormat:@",\"%@\":{", PARAM_ATTRIBUTES]]; + [headerString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"a" first:YES]]; + // >> Application and session information - [headerString appendString:[self formatAttributeWithName:PARAM_INSTALL_ID value:[self installationId] ]]; + // + [headerString appendString:[self formatAttributeWithName:PARAM_INSTALL_ID value:[self installationId] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey ]]; [headerString appendString:[self formatAttributeWithName:PARAM_APP_VERSION value:[self appVersion] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_LIBRARY_VERSION value:[self libraryVersion] ]]; - - // >> Device Information + + // >> Device Information + // if (device_uuid) { [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_UUID_HASHED value:[self hashString:device_uuid] ]]; } - if (device_adid) { - [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_ADID value:device_adid]]; - } + if (device_adid) { + [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_ADID value:device_adid]]; + } [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_PLATFORM value:[thisDevice model] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_OS_VERSION value:[thisDevice systemVersion] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_MODEL value:[self deviceModel] ]]; - -// MAC Address collection. Uncomment the following line to add Mac address to the mix of collected identifiers -// [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_MAC value:[self hashString:[self macAddress]] ]]; - [headerString appendString:[NSString stringWithFormat:@",\"%@\":%ld", PARAM_DEVICE_MEMORY, (long)[self availableMemory] ]]; + [headerString appendString:[NSString stringWithFormat:@",\"%@\":%lld", PARAM_DEVICE_MEMORY, (long long)[self availableMemory] ]]; [headerString appendString:[self formatAttributeWithName:PARAM_LOCALE_LANGUAGE value:device_language]]; [headerString appendString:[self formatAttributeWithName:PARAM_LOCALE_COUNTRY value:locale_country]]; [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_COUNTRY value:[locale objectForKey:NSLocaleCountryCode]]]; [headerString appendString:[NSString stringWithFormat:@",\"%@\":%@", PARAM_JAILBROKEN, [self isDeviceJailbroken] ? @"true" : @"false"]]; - - // Close second level - attributes - [headerString appendString:@"}"]; - - // Close first level - blob information - [headerString appendString:@"}\n"]; - + [headerString appendString:[NSString stringWithFormat:@",\"%@\":%d", PARAM_TIMEZONE_OFFSET, [[NSTimeZone localTimeZone] secondsFromGMT]]]; + + // Close second level - attributes + [headerString appendString:@"}"]; + + // >> Custom Identifiers + // + NSDictionary *identifiers = [[self db] identifiers]; + if(identifiers != nil) + { + // Open third level - properties + [headerString appendString:[NSString stringWithFormat:@",\"%@\":{", PARAM_IDENTIFIERS]]; + + int i =0; + for (id key in [identifiers allKeys]) + { + // Have to escape paramName and paramValue because they user-defined. + [headerString appendString: + [self formatAttributeWithName:[self escapeString:[key description]] + value:[self escapeString:[[identifiers valueForKey:key] description]] + first:(i == 0)]]; + i++; + } + + // Close third level - properties + [headerString appendString:@"}"]; + } + + // Close first level - blob information + [headerString appendString:@"}\n"]; + return [[headerString copy] autorelease]; } -- (BOOL)ll_isOptedIn { - return [[LocalyticsDatabase sharedLocalyticsDatabase] isOptedOut] == NO; +- (BOOL)ll_isOptedIn +{ + return [[self db] isOptedOut] == NO; } /*! @@ -804,17 +940,19 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @abstract Generates the JSON for an opt event (user opting in or out) and writes it to the database. @return YES if the event was written to the database, NO otherwise */ -- (BOOL)createOptEvent:(BOOL)optState { +- (BOOL)createOptEvent:(BOOL)optState +{ + // OptState is inversed. The JSON contains whether it is true that the user is opted out NSMutableString *optEventString = [NSMutableString string]; - [optEventString appendString:@"{"]; - [optEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"o" first:YES]]; + [optEventString appendString:@"{"]; + [optEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"o" first:YES]]; [optEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] first:NO ]]; - [optEventString appendString:[NSString stringWithFormat:@",\"%@\":%@", PARAM_OPT_VALUE, (optState ? @"false" : @"true") ]]; //this actually transmits the opposite of the opt state. The JSON contains whether the user is opted out, not whether the user is opted in. - [optEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; + [optEventString appendString:[NSString stringWithFormat:@",\"%@\":%@", PARAM_OPT_VALUE, (optState ? @"false" : @"true") ]]; + [optEventString appendFormat:@",\"%@\":%ld", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; [optEventString appendString:@"}\n"]; - - BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] addEventWithBlobString:[[optEventString copy] autorelease]]; - return success; + + BOOL success = [[self db] addEventWithBlobString:[[optEventString copy] autorelease]]; + return success; } /* @@ -824,68 +962,72 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @param removeOnResume YES if the application flow blob should be deleted if the session is resumed. @return YES if the application flow event was written to the database successfully. */ -- (BOOL)saveApplicationFlowAndRemoveOnResume:(BOOL)removeOnResume { - BOOL success = YES; - - // If there are no new events, then there is nothing additional to save. - if (self.unstagedFlowEvents.length) { - // Flows are uploaded as a distinct blob type containing arrays of new and previously-uploaded event and - // screen names. Write a flow event to the database. - NSMutableString *flowEventString = [[[NSMutableString alloc] init] autorelease]; - - // Open first level - flow blob event - [flowEventString appendString:@"{"]; - [flowEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"f" first:YES]]; - [flowEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]]; - [flowEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp]; - - // Open second level - new flow events - [flowEventString appendFormat:@",\"%@\":[", PARAM_NEW_FLOW_EVENTS]; - [flowEventString appendString:self.unstagedFlowEvents]; // Flow events are escaped in |-addFlowEventWithName:| - // Close second level - new flow events - [flowEventString appendString:@"]"]; - - // Open second level - old flow events - [flowEventString appendFormat:@",\"%@\":[", PARAM_OLD_FLOW_EVENTS]; - [flowEventString appendString:self.stagedFlowEvents]; - // Close second level - old flow events - [flowEventString appendString:@"]"]; - - // Close first level - flow blob event - [flowEventString appendString:@"}\n"]; - - success = [[LocalyticsDatabase sharedLocalyticsDatabase] addFlowEventWithBlobString:[[flowEventString copy] autorelease]]; - } - return success; +- (BOOL)saveApplicationFlowAndRemoveOnResume:(BOOL)removeOnResume +{ +#pragma unused(removeOnResume) + BOOL success = YES; + + // If there are no new events, then there is nothing additional to save. + if (self.unstagedFlowEvents.length) { + // Flows are uploaded as a distinct blob type containing arrays of new and previously-uploaded event and + // screen names. Write a flow event to the database. + NSMutableString *flowEventString = [[[NSMutableString alloc] init] autorelease]; + + // Open first level - flow blob event + [flowEventString appendString:@"{"]; + [flowEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"f" first:YES]]; + [flowEventString appendString:[self formatAttributeWithName:PARAM_UUID value:[self randomUUID] ]]; + [flowEventString appendFormat:@",\"%@\":%ld", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp]; + + // Open second level - new flow events + [flowEventString appendFormat:@",\"%@\":[", PARAM_NEW_FLOW_EVENTS]; + [flowEventString appendString:self.unstagedFlowEvents]; // Flow events are escaped in |-addFlowEventWithName:| + // Close second level - new flow events + [flowEventString appendString:@"]"]; + + // Open second level - old flow events + [flowEventString appendFormat:@",\"%@\":[", PARAM_OLD_FLOW_EVENTS]; + [flowEventString appendString:self.stagedFlowEvents]; + // Close second level - old flow events + [flowEventString appendString:@"]"]; + + // Close first level - flow blob event + [flowEventString appendString:@"}\n"]; + + success = [[self db] addFlowEventWithBlobString:[[flowEventString copy] autorelease]]; + } + return success; } // Convenience method for formatAttributeWithName which sets firstAttribute to NO since // this is the most common way to call it. -- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue { - return [self formatAttributeWithName:paramName value:paramValue first:NO]; +- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue +{ + return [self formatAttributeWithName:paramName value:paramValue first:NO]; } /*! @method formatAttributeWithName:value:firstAttribute: @abstract Returns the given string key/value pair as a JSON string. - @param paramName The name of the parameter + @param paramName The name of the parameter @param paramValue The value of the parameter @param firstAttribute YES if this attribute is first in an attribute list @return a JSON string which can be dumped to the JSON file */ -- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue first:(BOOL)firstAttribute { +- (NSString *)formatAttributeWithName:(NSString *)paramName value:(NSString *)paramValue first:(BOOL)firstAttribute +{ // The expected result is one of: - // "paramname":"paramvalue" - // "paramname":null - NSMutableString *formattedString = [NSMutableString string]; - if (!firstAttribute) { - [formattedString appendString:@","]; - } - - NSString *quotedString = @"\"%@\""; - paramName = [NSString stringWithFormat:quotedString, paramName]; - paramValue = paramValue ? [NSString stringWithFormat:quotedString, paramValue] : @"null"; - [formattedString appendFormat:@"%@:%@", paramName, paramValue]; + // "paramname":"paramvalue" + // "paramname":null + NSMutableString *formattedString = [NSMutableString string]; + if (!firstAttribute) { + [formattedString appendString:@","]; + } + + NSString *quotedString = @"\"%@\""; + paramName = [NSString stringWithFormat:quotedString, paramName]; + paramValue = paramValue ? [NSString stringWithFormat:quotedString, paramValue] : @"null"; + [formattedString appendFormat:@"%@:%@", paramName, paramValue]; return [[formattedString copy] autorelease]; } @@ -896,44 +1038,77 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @return The escaped version of the input string */ - (NSString *)escapeString:(NSString *)input -{ - NSString *output = [input stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; - output = [output stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; - output = [output stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"]; - output = [output stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; - output = [output stringByReplacingOccurrencesOfString:@"\t" withString:@"\\t"]; - output = [output stringByReplacingOccurrencesOfString:@"\b" withString:@"\\b"]; - output = [output stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; - output = [output stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"]; - output = [output stringByReplacingOccurrencesOfString:@"\v" withString:@"\\v"]; - return output; +{ + NSMutableString *escapedString = [NSMutableString stringWithCapacity:[input length] * 2]; + for(int i = 0; i < [input length]; i++) + { + unichar currentChar = [input characterAtIndex:i]; + switch(currentChar) + { + case '\\': + [escapedString appendString:@"\\\\"]; + break; + + case '\"': + [escapedString appendString:@"\\\""]; + break; + + case '\t': + [escapedString appendString:@"\\t"]; + break; + + case '\n': + [escapedString appendString:@"\\n"]; + break; + + case '\r': + [escapedString appendString:@"\\r"]; + break; + + case '\b': + [escapedString appendString:@"\\b"]; + break; + + case '\f': + [escapedString appendString:@"\\f"]; + break; + + default: + if (currentChar < 0x20) + [escapedString appendFormat:@"\\u%04x", currentChar]; + else + [escapedString appendFormat:@"%C", currentChar]; + } + } + + return [[escapedString copy] autorelease]; } - (void)applicationDidEnterBackground:(NSNotification *)notification { - [self logMessage:@"Application entered the background."]; - - // Continue executing until critical blocks finish executing or background time runs out, whichever comes first. - UIApplication *application = (UIApplication *)[notification object]; - __block UIBackgroundTaskIdentifier taskID = [application beginBackgroundTaskWithExpirationHandler:^{ - // Synchronize with the main queue in case the the tasks finish at the same time as the expiration handler. - dispatch_async(dispatch_get_main_queue(), ^{ - if (taskID != UIBackgroundTaskInvalid) { - [self logMessage:@"Failed to finish executing critical tasks. Cleaning up."]; - [application endBackgroundTask:taskID]; - taskID = UIBackgroundTaskInvalid; - } - }); - }]; - - // Critical tasks have finished. Expire the background task. - dispatch_group_notify(_criticalGroup, dispatch_get_main_queue(), ^{ - [self logMessage:@"Finished executing critical tasks."]; - if (taskID != UIBackgroundTaskInvalid) { - [application endBackgroundTask:taskID]; - taskID = UIBackgroundTaskInvalid; - } - }); + LocalyticsLog("Application entered the background."); + + // Continue executing until critical blocks finish executing or background time runs out, whichever comes first. + UIApplication *application = (UIApplication *)[notification object]; + __block UIBackgroundTaskIdentifier taskID = [application beginBackgroundTaskWithExpirationHandler:^{ + // Synchronize with the main queue in case the the tasks finish at the same time as the expiration handler. + dispatch_async(dispatch_get_main_queue(), ^{ + if (taskID != UIBackgroundTaskInvalid) { + LocalyticsLog("Failed to finish executing critical tasks. Cleaning up."); + [application endBackgroundTask:taskID]; + taskID = UIBackgroundTaskInvalid; + } + }); + }]; + + // Critical tasks have finished. Expire the background task. + dispatch_group_notify(_criticalGroup, dispatch_get_main_queue(), ^{ + LocalyticsLog("Finished executing critical tasks."); + if (taskID != UIBackgroundTaskInvalid) { + [application endBackgroundTask:taskID]; + taskID = UIBackgroundTaskInvalid; + } + }); } /*! @@ -941,11 +1116,9 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @abstract Logs a message with (localytics) prepended to it. @param message The message to log */ -- (void)logMessage:(NSString *)message ++ (void)logMessage:(NSString *)message { - if(DO_LOCALYTICS_LOGGING) { - NSLog(@"(localytics) %s\n", [message UTF8String]); - } + NSLog(@"\n(localytics) %@", message); } #pragma mark Datapoint Functions @@ -956,16 +1129,16 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; */ - (NSString *)customDimensions { - NSMutableString *dimensions = [[[NSMutableString alloc] init] autorelease]; - - for(int i=0; i <4; i++) { - NSString *dimension = [[LocalyticsDatabase sharedLocalyticsDatabase] customDimension:i]; - if(dimension) { - [dimensions appendFormat:@",\"c%i\":\"%@\"", i, dimension]; - } - } - - return [[dimensions copy] autorelease]; + NSMutableString *dimensions = [[[NSMutableString alloc] init] autorelease]; + + for(int i=0; i < 10; i++) { + NSString *dimension = [[self db] customDimension:i]; + if(dimension) { + [dimensions appendFormat:@",\"c%i\":\"%@\"", i, dimension]; + } + } + + return [[dimensions copy] autorelease]; } /*! @@ -973,60 +1146,14 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @abstract Returns the json blob containing the current location if available or nil if no location is available. */ - (NSString *)locationDimensions -{ - if(lastDeviceLocation.latitude == 0 || lastDeviceLocation.longitude == 0) { - return @""; - } - - return [NSString stringWithFormat:@",\"lat\":%f,\"lng\":%f", - lastDeviceLocation.latitude, - lastDeviceLocation.longitude]; - - - return [NSString stringWithFormat:@"%lf", lastDeviceLocation.latitude]; -} - -/*! - @method macAddress - @abstract Returns the macAddress of this device. - */ -- (NSString *)macAddress { - NSMutableString* result = [NSMutableString string]; - - BOOL success; - struct ifaddrs* addrs; - const struct ifaddrs* cursor; - const struct sockaddr_dl* dlAddr; - const uint8_t * base; - int i; - - success = (getifaddrs(&addrs) == 0); - if(success) - { - cursor = addrs; - while(cursor != NULL) - { - if((cursor->ifa_addr->sa_family == AF_LINK) && (((const struct sockaddr_dl *) cursor->ifa_addr)->sdl_type == IFT_ETHER)) - { - dlAddr = (const struct sockaddr_dl *) cursor->ifa_addr; - base = (const uint8_t *) &dlAddr->sdl_data[dlAddr->sdl_nlen]; - - for(i=0; isdl_alen; i++) - { - if(i != 0) { - [result appendString:@":"]; - } - [result appendFormat:@"%02x", base[i]]; - } - break; - } - cursor = cursor->ifa_next; - } - freeifaddrs(addrs); - } - - return result; + if(lastDeviceLocation.latitude == 0 || lastDeviceLocation.longitude == 0) { + return @""; + } + + return [NSString stringWithFormat:@",\"lat\":%f,\"lng\":%f", + lastDeviceLocation.latitude, + lastDeviceLocation.longitude]; } /*! @@ -1035,18 +1162,18 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; */ - (NSString *)hashString:(NSString *)input { - NSData *stringBytes = [input dataUsingEncoding: NSUTF8StringEncoding]; - unsigned char digest[CC_SHA1_DIGEST_LENGTH]; - - if (CC_SHA1([stringBytes bytes], [stringBytes length], digest)) { - NSMutableString* hashedUUID = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; - for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { - [hashedUUID appendFormat:@"%02x", digest[i]]; - } - return hashedUUID; - } - - return nil; + NSData *stringBytes = [input dataUsingEncoding: NSUTF8StringEncoding]; + unsigned char digest[CC_SHA1_DIGEST_LENGTH]; + + if (CC_SHA1([stringBytes bytes], [stringBytes length], digest)) { + NSMutableString* hashedUUID = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; + for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { + [hashedUUID appendFormat:@"%02x", digest[i]]; + } + return hashedUUID; + } + + return nil; } /*! @@ -1054,7 +1181,8 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @abstract Generates a random UUID @return NSString containing the new UUID */ -- (NSString *)randomUUID { +- (NSString *)randomUUID +{ CFUUIDRef theUUID = CFUUIDCreate(NULL); CFStringRef stringUUID = CFUUIDCreateString(NULL, theUUID); CFRelease(theUUID); @@ -1068,52 +1196,41 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; if not, it generates one. @return A string uniquely identifying this installation of this app */ -- (NSString *) installationId { - NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; - NSString *installId = [prefs stringForKey:PREFERENCES_KEY]; - - if(installId == nil) - { - [self logMessage:@"Install ID not found in preferences, checking DB"]; - installId = [[LocalyticsDatabase sharedLocalyticsDatabase] installId]; - } - - // If it hasn't been found yet, generate a new one. - if(installId == nil) - { - [self logMessage:@"Install ID not find one in database, generating a new one."]; - installId = [self randomUUID]; - } - - // Store the newly generated installId - [prefs setObject:installId forKey:PREFERENCES_KEY]; - [[NSUserDefaults standardUserDefaults] synchronize]; - - return installId; +- (NSString *)installationId +{ + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + NSString *installId = [prefs stringForKey:PREFERENCES_KEY]; + + if(installId == nil) + { + LocalyticsLog("Install ID not found in preferences, checking DB"); + installId = [[self db] installId]; + } + + // If it hasn't been found yet, generate a new one. + if(installId == nil) + { + LocalyticsLog("Install ID not find one in database, generating a new one."); + installId = [self randomUUID]; + } + + // Store the newly generated installId + [prefs setObject:installId forKey:PREFERENCES_KEY]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + return installId; } - /*! @method uniqueDeviceIdentifier @abstract A unique device identifier is a hash value composed from various hardware identifiers such - as the device’s serial number. It is guaranteed to be unique for every device but cannot + as the device’s serial number. It is guaranteed to be unique for every device but cannot be tied to a user account. [UIDevice Class Reference] @return An 1-way hashed identifier unique to this device. */ -- (NSString *)uniqueDeviceIdentifier { - - NSString *systemId = nil; - // We collect it as long as it is available along with a randomly generated ID. - // This way, when this becomes unavailable we can map existing users so the - // new vs returning counts do not break. - //only do this if the OS is less than 6.0 - if (([[[UIDevice currentDevice] systemVersion] floatValue] < 6.0f)) { - SEL udidSelector = NSSelectorFromString(@"uniqueIdentifier"); - if ([[UIDevice currentDevice] respondsToSelector:udidSelector]) { - systemId = [[UIDevice currentDevice] performSelector:udidSelector]; - } - } - return systemId; +- (NSString *)uniqueDeviceIdentifier +{ + return nil; } @@ -1124,24 +1241,39 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @return An identifier unique to this device. */ -- (NSString *)advertisingIdentifier { - NSString *adId = nil; +- (NSString *)advertisingIdentifier +{ + NSString *adId = nil; Class advertisingClass = NSClassFromString(@"ASIdentifierManager"); if (advertisingClass) { SEL adidSelector = NSSelectorFromString(@"advertisingIdentifier"); - adId = [[[advertisingClass performSelector:NSSelectorFromString(@"sharedManager")] performSelector:adidSelector] performSelector:NSSelectorFromString(@"UUIDString")]; + adId = [[[advertisingClass performSelector:NSSelectorFromString(@"sharedManager")] performSelector:adidSelector] performSelector:NSSelectorFromString(@"UUIDString")]; } return adId; } - /*! @method appVersion @abstract Gets the pretty string for this application's version. @return The application's version as a pretty string */ -- (NSString *)appVersion { - return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; +- (NSString *)appVersion +{ + return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; +} + +- (NSString *)customDimension:(int)dimension +{ + __block NSString *customDimension; + + dispatch_sync(self.queue, ^{ + @try { + customDimension = [self.db customDimension:dimension]; + } + @catch (NSException *e) {} + }); + + return customDimension; } /*! @@ -1149,8 +1281,9 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; @abstract Gets the current time as seconds since Unix epoch. @return an NSTimeInterval time. */ -- (NSTimeInterval)currentTimestamp { - return [[NSDate date] timeIntervalSince1970]; +- (NSTimeInterval)currentTimestamp +{ + return [[NSDate date] timeIntervalSince1970]; } /*! @@ -1159,44 +1292,46 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; of the jailbroken app sources. @return whether or not the device is jailbroken. */ -- (BOOL) isDeviceJailbroken { - NSFileManager *sessionFileManager = [NSFileManager defaultManager]; +- (BOOL)isDeviceJailbroken +{ + NSFileManager *sessionFileManager = [NSFileManager defaultManager]; return [sessionFileManager fileExistsAtPath:PATH_TO_APT]; } /*! @method deviceModel - @abstract Gets the device model string. + @abstract Gets the device model string. @return a platform string identifying the device */ -- (NSString *)deviceModel { +- (NSString *)deviceModel +{ char *buffer[256] = { 0 }; size_t size = sizeof(buffer); - sysctlbyname("hw.machine", buffer, &size, NULL, 0); - NSString *platform = [NSString stringWithCString:(const char*)buffer + sysctlbyname("hw.machine", buffer, &size, NULL, 0); + NSString *platform = [NSString stringWithCString:(const char*)buffer encoding:NSUTF8StringEncoding]; return platform; -} +} /*! @method modelSizeString @abstract Checks how much disk space is reported and uses that to determine the model @return A string identifying the model, e.g. 8GB, 16GB, etc */ -- (NSString *) modelSizeString { - +- (NSString *)modelSizeString +{ #if TARGET_IPHONE_SIMULATOR return @"simulator"; #endif // User partition NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSDictionary *stats = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[path lastObject] error:nil]; + NSDictionary *stats = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[path lastObject] error:nil]; uint64_t user = [[stats objectForKey:NSFileSystemSize] longLongValue]; // System partition path = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSSystemDomainMask, YES); - stats = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[path lastObject] error:nil]; + stats = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[path lastObject] error:nil]; uint64_t system = [[stats objectForKey:NSFileSystemSize] longLongValue]; // Add up and convert to gigabytes @@ -1213,19 +1348,55 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; /*! @method availableMemory - @abstract Reports how much memory is available + @abstract Reports how much memory is available @return A double containing the available free memory */ -- (double)availableMemory { +- (double)availableMemory +{ double result = NSNotFound; + vm_statistics_data_t stats; mach_msg_type_number_t count = HOST_VM_INFO_COUNT; if (!host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&stats, &count)) result = vm_page_size * stats.free_count; - + return result; } +/*! + @method appKeyIsValid + @abstract Reports whether the appKey is correctly formatted + @return A bool with the state of the app key. + */ ++ (BOOL)appKeyIsValid:(NSString *)appKey +{ + if(!appKey || appKey.length == 0) + return NO; + + NSPredicate *matchPred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", @"[A-Fa-f0-9-]+"]; + return [matchPred evaluateWithObject:appKey]; +} + +- (LocalyticsDatabase *)db +{ + @synchronized(self) { + if (_db == nil) { + _db = [[LocalyticsDatabase alloc] init]; + } + } + return _db; +} + +- (LocalyticsUploader *)uploader +{ + @synchronized(self) { + if (_uploader == nil) { + _uploader = [[LocalyticsUploader alloc] init]; + } + } + return _uploader; +} + #pragma mark System Functions + (id)allocWithZone:(NSZone *)zone { @@ -1239,47 +1410,46 @@ CLLocationCoordinate2D lastDeviceLocation = {0}; return nil; } -- (id)copyWithZone:(NSZone *)zone { +- (id)copyWithZone:(NSZone *)zone +{ +#pragma unused(zone) return self; } -- (id)retain { +- (id)retain +{ return self; } -- (unsigned)retainCount { +- (unsigned)retainCount +{ // maximum value of an unsigned int - prevents additional retains for the class return UINT_MAX; } -- (oneway void)release { - // ignore release commands -} +// Ignore release commands +- (oneway void)release {} -- (id)autorelease { +- (id)autorelease +{ return self; } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; - - dispatch_release(_criticalGroup); - dispatch_release(_queue); + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + + dispatch_release(_criticalGroup); + dispatch_release(_queue); [_sessionUUID release]; [_applicationKey release]; [_sessionCloseTime release]; - [_unstagedFlowEvents release]; - [_stagedFlowEvents release]; - [_screens release]; + [_unstagedFlowEvents release]; + [_stagedFlowEvents release]; + [_screens release]; [_sharedLocalyticsSession release]; - + [_localyticsDelegate release]; + [super dealloc]; } -#pragma mark - AMP stub -- (void)ampTrigger:(NSString *)event { - //do nothing -} - - @end diff --git a/External/Localytics/LocalyticsUploader.h b/External/Localytics/LocalyticsUploader.h old mode 100755 new mode 100644 index 260241a3..256b05fa --- a/External/Localytics/LocalyticsUploader.h +++ b/External/Localytics/LocalyticsUploader.h @@ -1,11 +1,11 @@ // LocalyticsUploader.h -// Copyright (C) 2012 Char Software Inc., DBA Localytics -// +// Copyright (C) 2013 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. +// with this source code. +// +// Please visit www.localytics.com for more information. #import @@ -19,15 +19,7 @@ extern NSString * const kLocalyticsKeyResponseBody; @interface LocalyticsUploader : NSObject { } -@property (readonly) BOOL isUploading; - -/*! - @method sharedLocalyticsUploader - @abstract Establishes this as a Singleton Class allowing for data persistence. - The class is accessed within the code using the following syntax: - [[LocalyticsUploader sharedLocalyticsUploader] functionHere] - */ -+ (LocalyticsUploader *)sharedLocalyticsUploader; +@property (readonly, atomic) BOOL isUploading; /*! @method LocalyticsUploader @@ -42,8 +34,9 @@ extern NSString * const kLocalyticsKeyResponseBody; @param localyticsApplicationKey the Localytics application ID @param useHTTPS Flag determining whether HTTP or HTTPS is used for the post URL. @param installId Install id passed to the server in the x-install-id header field. + @param libraryVersion Library version to be passed to the server in the x-client-version header field. */ -- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId; +- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId libraryVersion:(NSString *)libraryVersion; /*! @method LocalyticsUploader @@ -56,9 +49,27 @@ extern NSString * const kLocalyticsKeyResponseBody; @param localyticsApplicationKey the Localytics application ID @param useHTTPS Flag determining whether HTTP or HTTPS is used for the post URL. @param installId Install id passed to the server in the x-install-id header field. + @param libraryVersion Library version to be passed to the server in the x-client-version header field. @param resultTarget Result target is the target for the callback method that knows how to handle response data @param callback Callback is the method of the target class that is to be called with the data begin returned by an upload */ -- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId resultTarget:(id)target callback:(SEL)callbackMethod; +- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId libraryVersion:(NSString *)libraryVersion resultTarget:(id)target callback:(SEL)callbackMethod; -@end \ No newline at end of file + +/*! + @method LocalyticsUploader + @abstract Upload attribution data to Localytics. + @param localyticsApplicationKey the Localytics application ID + @param attribution Attribution cookie captured at install time + @param installId Install id passed to the server in the x-install-id header field. + @param advertisingIdentifier The Apple 'advertisingidentifier' + */ +- (void)uploaderAttributionWithApplicationKey:(NSString *)appKey attribution:(NSString *)attribution installId:(NSString *)installId advertisingIdentifier:(NSString *)advertisingIdentifier; + +/*! + @method uploadTimeStamp + @abstract Retrieve upload TimeStamp. + */ +- (NSString *)uploadTimeStamp; + +@end diff --git a/External/Localytics/LocalyticsUploader.m b/External/Localytics/LocalyticsUploader.m old mode 100755 new mode 100644 index 0b2f6127..8a71da05 --- a/External/Localytics/LocalyticsUploader.m +++ b/External/Localytics/LocalyticsUploader.m @@ -1,14 +1,15 @@ // LocalyticsUploader.m -// Copyright (C) 2012 Char Software Inc., DBA Localytics -// +// Copyright (C) 2013 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. -// +// with this source code. +// // Please visit www.localytics.com for more information. #import "LocalyticsUploader.h" #import "LocalyticsSession.h" +#import "LocalyticsSession+Private.h" #import "LocalyticsDatabase.h" #import "WebserviceConstants.h" #import @@ -20,15 +21,16 @@ #ifndef LOCALYTICS_URL_SECURED #define LOCALYTICS_URL_SECURED @"https://analytics.localytics.com/api/v2/applications/%@/uploads" #endif -static LocalyticsUploader *_sharedUploader = nil; + +#ifndef LOCALYTICS_ATTRIBUTION_SERVER +#define LOCALYTICS_ATTRIBUTION_SERVER @"http://a.localytics.com/fb_install/" +#endif NSString * const kLocalyticsKeyResponseBody = @"localytics.key.responseBody"; @interface LocalyticsUploader () - (void)finishUpload; - (NSData *)gzipDeflatedDataWithData:(NSData *)data; -- (void)logMessage:(NSString *)message; -- (NSString *)uploadTimeStamp; @property (readwrite) BOOL isUploading; @@ -37,146 +39,231 @@ NSString * const kLocalyticsKeyResponseBody = @"localytics.key.responseBody"; @implementation LocalyticsUploader @synthesize isUploading = _isUploading; -#pragma mark - Singleton Class -+ (LocalyticsUploader *)sharedLocalyticsUploader { - @synchronized(self) { - if (_sharedUploader == nil) { - _sharedUploader = [[self alloc] init]; - } - } - return _sharedUploader; -} - #pragma mark - Class Methods -- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId +- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId libraryVersion:(NSString *)libraryVersion { - [self uploaderWithApplicationKey:localyticsApplicationKey useHTTPS:useHTTPS installId:installId resultTarget:nil callback:NULL]; + [self uploaderWithApplicationKey:localyticsApplicationKey useHTTPS:useHTTPS installId:installId libraryVersion:libraryVersion resultTarget:nil callback:NULL]; } -- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId resultTarget:(id)target callback:(SEL)callbackMethod; + +- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey useHTTPS:(BOOL)useHTTPS installId:(NSString *)installId libraryVersion:(NSString *)libraryVersion resultTarget:(id)target callback:(SEL)callbackMethod { // Do nothing if already uploading. - if (self.isUploading == true) + if (self.isUploading == true) { - [self logMessage:@"Upload already in progress. Aborting."]; + LocalyticsLog("Upload already in progress. Aborting."); return; } - - [self logMessage:@"Beginning upload process"]; + + LocalyticsLog("Beginning upload process"); self.isUploading = true; // Prepare the data for upload. The upload could take a long time, so some effort has to be made to be sure that events // which get written while the upload is taking place don't get lost or duplicated. To achieve this, the logic is: - // 1) Append every header row blob string and and those of its associated events to the upload string. - // 2) Deflate and upload the data. - // 3) On success, delete all blob headers and staged events. Events added while an upload is in process are not - // deleted because they are not associated a header (and cannot be until the upload completes). - - // Step 1 - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - - LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase]; - NSString *blobString = [db uploadBlobString]; - - if ([blobString length] == 0) { - // There is nothing outstanding to upload. - [self logMessage:@"Abandoning upload. There are no new events."]; - [pool drain]; - [self finishUpload]; - - return; - } - + // 1) Append every header row blob string and and those of its associated events to the upload string. + // 2) Deflate and upload the data. + // 3) On success, delete all blob headers and staged events. Events added while an upload is in process are not + // deleted because they are not associated a header (and cannot be until the upload completes). + + // Step 1 + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + LocalyticsDatabase *db = [[LocalyticsSession shared] db]; + NSString *blobString = [db uploadBlobString]; + + if ([blobString length] == 0) { + // There is nothing outstanding to upload. + LocalyticsLog("Abandoning upload. There are no new events."); + [pool drain]; + [self finishUpload]; + + return; + } + NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding]; - NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease]; - [self logMessage:[NSString stringWithFormat:@"Uploading data (length: %u)", [myString length]]]; - [self logMessage:myString]; - - // Step 2 - NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain]; - - [pool drain]; + if(LOCALYTICS_LOGGING_ENABLED) { + NSString *logString = [[[NSString alloc] initWithData:requestData + encoding:NSUTF8StringEncoding] autorelease]; + NSUInteger stringLength = [logString length]; + + logString = [logString stringByReplacingOccurrencesOfString:@"{" + withString:@"\n\t{"]; + logString = [logString stringByReplacingOccurrencesOfString:@",\"" + withString:@",\n\t\""]; + + LocalyticsLog("Uploading data (length: %u)\n%@", + stringLength, + logString); + } + + // Step 2 + NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain]; + + [pool drain]; + + NSString *urlStringFormat; + if (useHTTPS) { + urlStringFormat = LOCALYTICS_URL_SECURED; + } else { + urlStringFormat = LOCALYTICS_URL; + } + NSURL *apiUrl = [NSURL URLWithString:[NSString stringWithFormat:urlStringFormat,[localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; + NSMutableURLRequest *submitRequest = [self createRequestWithURL:apiUrl + installId:installId + libraryVersion:libraryVersion + requestData:deflatedRequestData]; + + [deflatedRequestData release]; + + // Perform synchronous upload in an async dispatch. This is necessary because the calling block will not persist to + // receive the response data. + dispatch_group_async([[LocalyticsSession shared] criticalGroup], [[LocalyticsSession shared] queue], ^{ + @try { + NSURLResponse *response = nil; + NSError *responseError = nil; + NSData *responseData = [NSURLConnection sendSynchronousRequest:submitRequest returningResponse:&response error:&responseError]; + NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode]; + + if (responseError) { + // On error, simply print the error and close the uploader. We have to assume the data was not transmited + // so it is not deleted. In the event that we accidently store data which was succesfully uploaded, the + // duplicate data will be ignored by the server when it is next uploaded. + LocalyticsLog("Error Uploading. Code: %d, Description: %@", + [responseError code], + [responseError localizedDescription]); + } else { + // Step 3 + // While response status codes in the 5xx range leave upload rows intact, the default case is to delete. + if (responseStatusCode >= 500 && responseStatusCode < 600) { + LocalyticsLog("Upload failed with response status code %d", responseStatusCode); + } else { + // Because only one instance of the uploader can be running at a time it should not be possible for + // new upload rows to appear so there is no fear of deleting data which has not yet been uploaded. + LocalyticsLog("Upload completed successfully. Response code %d", responseStatusCode); + [[[LocalyticsSession shared] db] deleteUploadedData]; + } + } + + if ([responseData length] > 0) { + if (LOCALYTICS_LOGGING_ENABLED) { + NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; + LocalyticsLog("Response body: %@", responseString); + [responseString release]; + } + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:responseData forKey:kLocalyticsKeyResponseBody]; + if (target) { + [target performSelector:callbackMethod withObject:userInfo]; + } + } + + [self finishUpload]; + } + @catch (NSException * e) {} + }); +} - NSString *urlStringFormat; - if (useHTTPS) { - urlStringFormat = LOCALYTICS_URL_SECURED; - } else { - urlStringFormat = LOCALYTICS_URL; - } - NSString *apiUrlString = [NSString stringWithFormat:urlStringFormat, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString] - cachePolicy:NSURLRequestReloadIgnoringCacheData - timeoutInterval:60.0]; +- (NSMutableURLRequest *)createRequestWithURL:(NSURL *)URL installId:(NSString *)installId libraryVersion:(NSString *)libraryVersion requestData:(NSData *)requestData +{ + + NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:URL + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:60.0]; [submitRequest setHTTPMethod:@"POST"]; - [submitRequest setValue:[self uploadTimeStamp] forHTTPHeaderField:HEADER_CLIENT_TIME]; - [submitRequest setValue:installId forHTTPHeaderField:HEADER_INSTALL_ID]; + [submitRequest setValue:[self uploadTimeStamp] forHTTPHeaderField:HEADER_CLIENT_TIME]; + [submitRequest setValue:installId forHTTPHeaderField:HEADER_INSTALL_ID]; + [submitRequest setValue:libraryVersion forHTTPHeaderField:HEADER_CLIENT_VERSION]; [submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"]; - [submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"]; - [submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"]; - [submitRequest setHTTPBody:deflatedRequestData]; - [deflatedRequestData release]; - - // Perform synchronous upload in an async dispatch. This is necessary because the calling block will not persist to - // receive the response data. - dispatch_group_async([[LocalyticsSession sharedLocalyticsSession] criticalGroup], [[LocalyticsSession sharedLocalyticsSession] queue], ^{ - @try { - NSURLResponse *response = nil; - NSError *responseError = nil; - NSData *responseData = [NSURLConnection sendSynchronousRequest:submitRequest returningResponse:&response error:&responseError]; - NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode]; - - if (responseError) { - // On error, simply print the error and close the uploader. We have to assume the data was not transmited - // so it is not deleted. In the event that we accidently store data which was succesfully uploaded, the - // duplicate data will be ignored by the server when it is next uploaded. - [self logMessage:[NSString stringWithFormat: - @"Error Uploading. Code: %d, Description: %@", - [responseError code], - [responseError localizedDescription]]]; - } else { - // Step 3 - // While response status codes in the 5xx range leave upload rows intact, the default case is to delete. - if (responseStatusCode >= 500 && responseStatusCode < 600) { - [self logMessage:[NSString stringWithFormat:@"Upload failed with response status code %d", responseStatusCode]]; - } else { - // Because only one instance of the uploader can be running at a time it should not be possible for - // new upload rows to appear so there is no fear of deleting data which has not yet been uploaded. - [self logMessage:[NSString stringWithFormat:@"Upload completed successfully. Response code %d", responseStatusCode]]; - [[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadedData]; - } - } - - if ([responseData length] > 0) { - if (DO_LOCALYTICS_LOGGING) { - NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; - [self logMessage:[NSString stringWithFormat:@"Response body: %@", responseString]]; - [responseString release]; - } - NSDictionary *userInfo = [NSDictionary dictionaryWithObject:responseData forKey:kLocalyticsKeyResponseBody]; - if (target) { - [target performSelector:callbackMethod withObject:userInfo]; - } - } - } - @catch (NSException * e) {} - - [self finishUpload]; - }); + [submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"]; + [submitRequest setValue:[NSString stringWithFormat:@"%d", requestData.length] forHTTPHeaderField:@"Content-Length"]; + + if ([LocalyticsSession shared].delaySession == YES) + { + [submitRequest setValue:@"true" forHTTPHeaderField:HEADER_DELAY_SESSION]; + } + + [submitRequest setHTTPBody:requestData]; + + return submitRequest; } - (void)finishUpload { - self.isUploading = false; - - // Upload data has been deleted. Recover the disk space if necessary. - [[LocalyticsDatabase sharedLocalyticsDatabase] vacuumIfRequired]; + self.isUploading = false; + + // Upload data has been deleted. Recover the disk space if necessary. + [[[LocalyticsSession shared] db] vacuumIfRequired]; } +- (void)uploaderAttributionWithApplicationKey:(NSString *)appKey attribution:(NSString *)attribution installId:(NSString *)installId advertisingIdentifier:(NSString *)advertisingIdentifier +{ + // Required parameters + if(!attribution) + return; + + NSString *apiUrlString = [LOCALYTICS_ATTRIBUTION_SERVER stringByAppendingString:appKey]; + NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString] + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:60.0]; + + NSMutableString *postBody = [NSMutableString string]; + [postBody appendFormat:@"%@=%@", FB_ATTRIBUTION, attribution]; + [postBody appendFormat:@"&%@=%ld", FB_ATTRIBUTION_TIME, (long)[[[LocalyticsSession shared] db] createdTimestamp]]; + + if(advertisingIdentifier) + { + [postBody appendFormat:@"&%@=%@", FB_DEVICE_ID_TYPE, @"adid"]; + [postBody appendFormat:@"&%@=%@", FB_DEVICE_ID, advertisingIdentifier]; + + } + + if(installId) + { + [postBody appendFormat:@"&%@=%@", FB_INSTALL_ID, installId]; + } + + [submitRequest setHTTPMethod:@"POST"]; + [submitRequest setHTTPBody:[postBody dataUsingEncoding:NSUTF8StringEncoding]]; + + // Perform synchronous upload in an async dispatch. This is necessary because the calling block will not persist to + // receive the response data. + dispatch_group_async([[LocalyticsSession shared] criticalGroup], [[LocalyticsSession shared] queue], ^{ + @try { + NSURLResponse *response = nil; + NSError *responseError = nil; + [NSURLConnection sendSynchronousRequest:submitRequest + returningResponse:&response + error:&responseError]; + NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode]; + + if (responseError) { + // On error, simply print the error and close the uploader. We have to assume the data was not transmited + // so it is not deleted. + LocalyticsLog("Error uploading Facebook attribution. Code: %d, Description: %@", + [responseError code], + [responseError localizedDescription]); + } + else + { + // While response status codes in the 5xx range leave upload rows intact, the default case is to delete. + if (responseStatusCode >= 500 && responseStatusCode < 600) { + LocalyticsLog("Facebook attribution upload unsuccessful. Response code %d", responseStatusCode); + } + else + { + LocalyticsLog("Facebook attribution upload completed successfully. Response code %d", responseStatusCode); + [[[LocalyticsSession shared] db] setFacebookAttribution:nil]; + [LocalyticsSession shared].facebookAttribution = nil; + } + } + } + @catch (NSException * e) {} + }); +} /*! @method gzipDeflatedDataWithData - @abstract Deflates the provided data using gzip at the default compression level (6). Complete NSData gzip category available on CocoaDev. http://www.cocoadev.com/index.pl?NSDataCategory. + @abstract Deflates the provided data using gzip at the default compression level (6). @return the deflated data */ - (NSData *)gzipDeflatedDataWithData:(NSData *)data @@ -210,7 +297,7 @@ NSString * const kLocalyticsKeyResponseBody = @"localytics.key.responseBody"; strm.next_out = [compressed mutableBytes] + strm.total_out; strm.avail_out = [compressed length] - strm.total_out; - deflate(&strm, Z_FINISH); + deflate(&strm, Z_FINISH); } while (strm.avail_out == 0); @@ -220,39 +307,19 @@ NSString * const kLocalyticsKeyResponseBody = @"localytics.key.responseBody"; return [NSData dataWithData:compressed]; } -/*! - @method logMessage - @abstract Logs a message with (localytics uploader) prepended to it - @param message The message to log - */ -- (void) logMessage:(NSString *)message { - if(DO_LOCALYTICS_LOGGING) { - NSLog(@"(localytics uploader) %s\n", [message UTF8String]); - } -} - /*! @method uploadTimeStamp - @abstract Gets the current time, along with local timezone, formatted as a DateTime for the webservice. + @abstract Gets the current time, along with local timezone, formatted as a DateTime for the webservice. @return a DateTime of the current local time and timezone. */ - (NSString *)uploadTimeStamp { - return [ NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970] ]; + return [ NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970] ]; } #pragma mark - System Functions -+ (id)allocWithZone:(NSZone *)zone { - @synchronized(self) { - if (_sharedUploader == nil) { - _sharedUploader = [super allocWithZone:zone]; - return _sharedUploader; - } - } - // returns nil on subsequent allocations - return nil; -} - (id)copyWithZone:(NSZone *)zone { +#pragma unused(zone) return self; } @@ -273,9 +340,4 @@ NSString * const kLocalyticsKeyResponseBody = @"localytics.key.responseBody"; return self; } -- (void)dealloc { - [_sharedUploader release]; - [super dealloc]; -} - @end diff --git a/External/Localytics/WebserviceConstants.h b/External/Localytics/WebserviceConstants.h old mode 100755 new mode 100644 index 5f53f9fa..da62b16b --- a/External/Localytics/WebserviceConstants.h +++ b/External/Localytics/WebserviceConstants.h @@ -1,21 +1,23 @@ // WebserviceConstants.h -// Copyright (C) 2012 Char Software Inc., DBA Localytics -// +// Copyright (C) 2013 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. -// +// with this source code. +// // Please visit www.localytics.com for more information. // The constants which are used to make up the JSON blob // To save disk space and network bandwidth all the keywords have been -// abbreviated and are exploded by the server. +// abbreviated and are exploded by the server. /***************** * Upload Header * *****************/ -#define HEADER_CLIENT_TIME @"x-upload-time" -#define HEADER_INSTALL_ID @"x-install-id" +#define HEADER_CLIENT_TIME @"x-upload-time" +#define HEADER_INSTALL_ID @"x-install-id" +#define HEADER_CLIENT_VERSION @"x-client-version" +#define HEADER_DELAY_SESSION @"ll-first-session" /********************* * Shared Attributes * @@ -38,7 +40,7 @@ // PARAM_DATA_TYPE => "h" for Header // PARAM_ATTRIBUTES => dictionary containing Header Common Attributes #define PARAM_PERSISTED_AT @"pa" // Persistent Storage Created At. A timestamp created when the app was - // first launched and the persistent storage was created. Stores as + // first launched and the persistent storage was created. Stores as // seconds from Unix epoch. (int) #define PARAM_SEQUENCE_NUMBER @"seq" // Sequence number - an increasing count for each blob, stored in the // persistent store Consistent across app starts. (int) @@ -62,8 +64,11 @@ #define PARAM_DEVICE_MODEL @"dmo" // Device Model #define PARAM_DEVICE_OS_VERSION @"dov" // Device OS Version #define PARAM_NETWORK_CARRIER @"nca" // Network Carrier -#define PARAM_OPT_VALUE @"out" // Opt Out (boolean) +#define PARAM_OPT_VALUE @"out" // Opt Out (boolean) #define PARAM_DEVICE_MEMORY @"dmem" // Device Memory +#define PARAM_IDENTIFIERS @"ids" // Identifiers (dictionary) +#define PARAM_BIRTH_TIME @"b" // Birth time (Since epoch) +#define PARAM_TIMEZONE_OFFSET @"tz" // Device offset from GMT in seconds /***************** * Session Start * @@ -101,6 +106,7 @@ // PARAM_ATTRIBUTES => dictionary containing attributes for this event as key-value string pairs #define PARAM_EVENT_NAME @"n" // Event Name, (eg. 'Button Click') #define PARAM_REPORT_ATTRIBUTES @"rattrs" // Attributes used in custom reports +#define PARAM_VALUE_NAME @"v" // Added customer value for an event, such as revenue /******************** * Application flow * @@ -111,4 +117,17 @@ // PARAM_CLIENT_TIME #define PARAM_SESSION_START @"ss" // Start time for the current session. #define PARAM_NEW_FLOW_EVENTS @"nw" // Events and screens encountered during this session that have NOT been staged for upload. -#define PARAM_OLD_FLOW_EVENTS @"od" // Events and screens encountered during this session that HAVE been staged for upload. \ No newline at end of file +#define PARAM_OLD_FLOW_EVENTS @"od" // Events and screens encountered during this session that HAVE been staged for upload. + +/************************ + * Partner attributions * + ***********************/ +#define FB_ATTRIBUTION @"fb_attrib_first" // Facebook attribution cookie +#define FB_ATTRIBUTION_TIME @"fb_attrib_first_date" // Time original attribution cookie was collected +#define FB_ATTRIBUTION_CURRENT @"fb_attrib_current" // Facebook attribution cookie +#define FB_ATTRIBUTION_CURRENT_TIME @"fb_attrib_current_date" // Time original attribution cookie was collected +#define FB_DEVICE_ID @"dpid" // Device unique identifiers +#define FB_DEVICE_ID_TYPE @"dpid_type" // Either UDID or ADID (advertisingIdentifier) +#define FB_INSTALL_ID @"install_id" // Device install ID + + diff --git a/MasterPassword/ObjC/MPAppDelegate_Key.m b/MasterPassword/ObjC/MPAppDelegate_Key.m index 0293eb8b..842d9ac8 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Key.m +++ b/MasterPassword/ObjC/MPAppDelegate_Key.m @@ -142,6 +142,9 @@ static NSDictionary *keyQuery(MPUserEntity *user) { #ifdef CRASHLYTICS [Crashlytics setObjectValue:user.userID forKey:@"username"]; [Crashlytics setUserName:user.userID]; +#endif +#if TARGET_OS_IPHONE + [[LocalyticsSession sharedLocalyticsSession] setCustomerName:user.userID]; #endif } } diff --git a/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m b/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m index 6b72529c..bf5ee418 100644 --- a/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m +++ b/MasterPassword/ObjC/iOS/MPiOSAppDelegate.m @@ -11,6 +11,7 @@ #import "MPAppDelegate_Store.h" #import "IASKSettingsReader.h" +#import "LocalyticsAmpSession.h" @interface MPiOSAppDelegate() @@ -116,6 +117,8 @@ [[LocalyticsSession sharedLocalyticsSession] LocalyticsSession:localyticsKey]; [[LocalyticsSession sharedLocalyticsSession] open]; [LocalyticsSession sharedLocalyticsSession].enableHTTPS = YES; + [[LocalyticsSession sharedLocalyticsSession] setCustomerId:[PearlKeyChain deviceIdentifier]]; + [[LocalyticsSession sharedLocalyticsSession] setCustomerName:@"Anonymous"]; [[LocalyticsSession sharedLocalyticsSession] upload]; [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { if (message.level >= PearlLogLevelWarn) @@ -279,6 +282,10 @@ if ([self.googlePlus handleURL:url sourceApplication:sourceApplication annotation:annotation]) return YES; + // Localytics + if ([[LocalyticsAmpSession shared] handleURL:url]) + return YES; + // Arbitrary URL to mpsites data. dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{ NSError *error; diff --git a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj index 1ab2d9a6..c8c79b28 100644 --- a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -811,13 +811,6 @@ DACA22BD1705DE7D002C6C22 /* NSError+UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DACA22B91705DE7D002C6C22 /* NSError+UbiquityStoreManager.m */; }; DACA22BE1705DE7D002C6C22 /* UbiquityStoreManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DACA22BA1705DE7D002C6C22 /* UbiquityStoreManager.h */; }; DACA22C61705DE9D002C6C22 /* libTestFlight.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DACA22C31705DE9D002C6C22 /* libTestFlight.a */; }; - DACA22CF1705DEB0002C6C22 /* LocalyticsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = DACA22C81705DEB0002C6C22 /* LocalyticsDatabase.m */; }; - DACA22D01705DEB0002C6C22 /* LocalyticsSession.h in Headers */ = {isa = PBXBuildFile; fileRef = DACA22C91705DEB0002C6C22 /* LocalyticsSession.h */; }; - DACA22D11705DEB0002C6C22 /* WebserviceConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = DACA22CA1705DEB0002C6C22 /* WebserviceConstants.h */; }; - DACA22D21705DEB0002C6C22 /* LocalyticsUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = DACA22CB1705DEB0002C6C22 /* LocalyticsUploader.m */; }; - DACA22D31705DEB0002C6C22 /* LocalyticsUploader.h in Headers */ = {isa = PBXBuildFile; fileRef = DACA22CC1705DEB0002C6C22 /* LocalyticsUploader.h */; }; - DACA22D41705DEB0002C6C22 /* LocalyticsDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = DACA22CD1705DEB0002C6C22 /* LocalyticsDatabase.h */; }; - DACA22D51705DEB0002C6C22 /* LocalyticsSession.m in Sources */ = {isa = PBXBuildFile; fileRef = DACA22CE1705DEB0002C6C22 /* LocalyticsSession.m */; }; DACA296C1705DF81002C6C22 /* TestFlight.plist in Resources */ = {isa = PBXBuildFile; fileRef = DACA26941705DF81002C6C22 /* TestFlight.plist */; }; DACA296D1705DF81002C6C22 /* Localytics.plist in Resources */ = {isa = PBXBuildFile; fileRef = DACA26961705DF81002C6C22 /* Localytics.plist */; }; DACA296E1705DF81002C6C22 /* Google+.plist in Resources */ = {isa = PBXBuildFile; fileRef = DACA26981705DF81002C6C22 /* Google+.plist */; }; @@ -850,6 +843,17 @@ DAFC568F172C57EC00CB5CC5 /* IASKSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC5680172C57EC00CB5CC5 /* IASKSwitch.m */; }; DAFC5690172C57EC00CB5CC5 /* IASKTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC5682172C57EC00CB5CC5 /* IASKTextField.m */; }; DAFC5691172C582A00CB5CC5 /* libInAppSettingsKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAFC5655172C573B00CB5CC5 /* libInAppSettingsKit.a */; }; + DAFC56A2172C6E8500CB5CC5 /* LocalyticsDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFC569A172C6E8500CB5CC5 /* LocalyticsDatabase.h */; }; + DAFC56A3172C6E8500CB5CC5 /* LocalyticsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC569B172C6E8500CB5CC5 /* LocalyticsDatabase.m */; }; + DAFC56A4172C6E8500CB5CC5 /* LocalyticsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC569B172C6E8500CB5CC5 /* LocalyticsDatabase.m */; }; + DAFC56A5172C6E8500CB5CC5 /* LocalyticsSession.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFC569C172C6E8500CB5CC5 /* LocalyticsSession.h */; }; + DAFC56A6172C6E8500CB5CC5 /* LocalyticsSession.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC569D172C6E8500CB5CC5 /* LocalyticsSession.m */; }; + DAFC56A7172C6E8500CB5CC5 /* LocalyticsSession.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC569D172C6E8500CB5CC5 /* LocalyticsSession.m */; }; + DAFC56A8172C6E8500CB5CC5 /* LocalyticsSession+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFC569E172C6E8500CB5CC5 /* LocalyticsSession+Private.h */; }; + DAFC56A9172C6E8500CB5CC5 /* LocalyticsUploader.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFC569F172C6E8500CB5CC5 /* LocalyticsUploader.h */; }; + DAFC56AA172C6E8500CB5CC5 /* LocalyticsUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC56A0172C6E8500CB5CC5 /* LocalyticsUploader.m */; }; + DAFC56AB172C6E8500CB5CC5 /* LocalyticsUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFC56A0172C6E8500CB5CC5 /* LocalyticsUploader.m */; }; + DAFC56AC172C6E8500CB5CC5 /* WebserviceConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFC56A1172C6E8500CB5CC5 /* WebserviceConstants.h */; }; DAFE4A1315039824003ABA7C /* NSObject+PearlExport.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFE45D815039823003ABA7C /* NSObject+PearlExport.h */; }; DAFE4A1415039824003ABA7C /* NSObject+PearlExport.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFE45D915039823003ABA7C /* NSObject+PearlExport.m */; }; DAFE4A1515039824003ABA7C /* NSString+PearlNSArrayFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = DAFE45DA15039823003ABA7C /* NSString+PearlNSArrayFormat.h */; }; @@ -1838,13 +1842,6 @@ DACA22BA1705DE7D002C6C22 /* UbiquityStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UbiquityStoreManager.h; sourceTree = ""; }; DACA22C11705DE9D002C6C22 /* TestFlight.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestFlight.h; sourceTree = ""; }; DACA22C31705DE9D002C6C22 /* libTestFlight.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libTestFlight.a; sourceTree = ""; }; - DACA22C81705DEB0002C6C22 /* LocalyticsDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsDatabase.m; sourceTree = ""; }; - DACA22C91705DEB0002C6C22 /* LocalyticsSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsSession.h; sourceTree = ""; }; - DACA22CA1705DEB0002C6C22 /* WebserviceConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebserviceConstants.h; sourceTree = ""; }; - DACA22CB1705DEB0002C6C22 /* LocalyticsUploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsUploader.m; sourceTree = ""; }; - DACA22CC1705DEB0002C6C22 /* LocalyticsUploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsUploader.h; sourceTree = ""; }; - DACA22CD1705DEB0002C6C22 /* LocalyticsDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsDatabase.h; sourceTree = ""; }; - DACA22CE1705DEB0002C6C22 /* LocalyticsSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsSession.m; sourceTree = ""; }; DACA26941705DF81002C6C22 /* TestFlight.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestFlight.plist; sourceTree = ""; }; DACA26961705DF81002C6C22 /* Localytics.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Localytics.plist; sourceTree = ""; }; DACA26981705DF81002C6C22 /* Google+.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Google+.plist"; sourceTree = ""; }; @@ -1893,6 +1890,14 @@ DAFC5680172C57EC00CB5CC5 /* IASKSwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IASKSwitch.m; sourceTree = ""; }; DAFC5681172C57EC00CB5CC5 /* IASKTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IASKTextField.h; sourceTree = ""; }; DAFC5682172C57EC00CB5CC5 /* IASKTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IASKTextField.m; sourceTree = ""; }; + DAFC569A172C6E8500CB5CC5 /* LocalyticsDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsDatabase.h; sourceTree = ""; }; + DAFC569B172C6E8500CB5CC5 /* LocalyticsDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsDatabase.m; sourceTree = ""; }; + DAFC569C172C6E8500CB5CC5 /* LocalyticsSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsSession.h; sourceTree = ""; }; + DAFC569D172C6E8500CB5CC5 /* LocalyticsSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsSession.m; sourceTree = ""; }; + DAFC569E172C6E8500CB5CC5 /* LocalyticsSession+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LocalyticsSession+Private.h"; sourceTree = ""; }; + DAFC569F172C6E8500CB5CC5 /* LocalyticsUploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsUploader.h; sourceTree = ""; }; + DAFC56A0172C6E8500CB5CC5 /* LocalyticsUploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsUploader.m; sourceTree = ""; }; + DAFC56A1172C6E8500CB5CC5 /* WebserviceConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebserviceConstants.h; sourceTree = ""; }; DAFE45D815039823003ABA7C /* NSObject+PearlExport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+PearlExport.h"; sourceTree = ""; }; DAFE45D915039823003ABA7C /* NSObject+PearlExport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+PearlExport.m"; sourceTree = ""; }; DAFE45DA15039823003ABA7C /* NSString+PearlNSArrayFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+PearlNSArrayFormat.h"; sourceTree = ""; }; @@ -3176,13 +3181,14 @@ DACA22C71705DEB0002C6C22 /* Localytics */ = { isa = PBXGroup; children = ( - DACA22C81705DEB0002C6C22 /* LocalyticsDatabase.m */, - DACA22C91705DEB0002C6C22 /* LocalyticsSession.h */, - DACA22CA1705DEB0002C6C22 /* WebserviceConstants.h */, - DACA22CB1705DEB0002C6C22 /* LocalyticsUploader.m */, - DACA22CC1705DEB0002C6C22 /* LocalyticsUploader.h */, - DACA22CD1705DEB0002C6C22 /* LocalyticsDatabase.h */, - DACA22CE1705DEB0002C6C22 /* LocalyticsSession.m */, + DAFC569A172C6E8500CB5CC5 /* LocalyticsDatabase.h */, + DAFC569B172C6E8500CB5CC5 /* LocalyticsDatabase.m */, + DAFC569C172C6E8500CB5CC5 /* LocalyticsSession.h */, + DAFC569D172C6E8500CB5CC5 /* LocalyticsSession.m */, + DAFC569E172C6E8500CB5CC5 /* LocalyticsSession+Private.h */, + DAFC569F172C6E8500CB5CC5 /* LocalyticsUploader.h */, + DAFC56A0172C6E8500CB5CC5 /* LocalyticsUploader.m */, + DAFC56A1172C6E8500CB5CC5 /* WebserviceConstants.h */, ); path = Localytics; sourceTree = ""; @@ -3590,10 +3596,11 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - DACA22D01705DEB0002C6C22 /* LocalyticsSession.h in Headers */, - DACA22D11705DEB0002C6C22 /* WebserviceConstants.h in Headers */, - DACA22D31705DEB0002C6C22 /* LocalyticsUploader.h in Headers */, - DACA22D41705DEB0002C6C22 /* LocalyticsDatabase.h in Headers */, + DAFC56A2172C6E8500CB5CC5 /* LocalyticsDatabase.h in Headers */, + DAFC56A5172C6E8500CB5CC5 /* LocalyticsSession.h in Headers */, + DAFC56A8172C6E8500CB5CC5 /* LocalyticsSession+Private.h in Headers */, + DAFC56A9172C6E8500CB5CC5 /* LocalyticsUploader.h in Headers */, + DAFC56AC172C6E8500CB5CC5 /* WebserviceConstants.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4733,9 +4740,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DACA22CF1705DEB0002C6C22 /* LocalyticsDatabase.m in Sources */, - DACA22D21705DEB0002C6C22 /* LocalyticsUploader.m in Sources */, - DACA22D51705DEB0002C6C22 /* LocalyticsSession.m in Sources */, + DAFC56A3172C6E8500CB5CC5 /* LocalyticsDatabase.m in Sources */, + DAFC56A6172C6E8500CB5CC5 /* LocalyticsSession.m in Sources */, + DAFC56AA172C6E8500CB5CC5 /* LocalyticsUploader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4757,6 +4764,9 @@ DAFC568E172C57EC00CB5CC5 /* IASKSlider.m in Sources */, DAFC568F172C57EC00CB5CC5 /* IASKSwitch.m in Sources */, DAFC5690172C57EC00CB5CC5 /* IASKTextField.m in Sources */, + DAFC56A4172C6E8500CB5CC5 /* LocalyticsDatabase.m in Sources */, + DAFC56A7172C6E8500CB5CC5 /* LocalyticsSession.m in Sources */, + DAFC56AB172C6E8500CB5CC5 /* LocalyticsUploader.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5321,6 +5331,10 @@ isa = XCBuildConfiguration; buildSettings = { GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../External/Localytics\"", + ); }; name = "Debug-iOS"; }; @@ -5328,6 +5342,10 @@ isa = XCBuildConfiguration; buildSettings = { GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../External/Localytics\"", + ); }; name = "AdHoc-iOS"; }; @@ -5335,6 +5353,10 @@ isa = XCBuildConfiguration; buildSettings = { GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../External/Localytics\"", + ); }; name = "AppStore-iOS"; }; @@ -5342,6 +5364,10 @@ isa = XCBuildConfiguration; buildSettings = { GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../External/Localytics\"", + ); }; name = "Debug-iOS"; }; @@ -5349,6 +5375,10 @@ isa = XCBuildConfiguration; buildSettings = { GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../External/Localytics\"", + ); }; name = "AdHoc-iOS"; }; @@ -5356,6 +5386,10 @@ isa = XCBuildConfiguration; buildSettings = { GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../External/Localytics\"", + ); }; name = "AppStore-iOS"; };