diff --git a/Crashlytics/Crashlytics.framework/Crashlytics b/Crashlytics/Crashlytics.framework/Crashlytics new file mode 120000 index 00000000..7074275f --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Crashlytics @@ -0,0 +1 @@ +Versions/Current/Crashlytics \ No newline at end of file diff --git a/Crashlytics/Crashlytics.framework/Headers b/Crashlytics/Crashlytics.framework/Headers new file mode 120000 index 00000000..a177d2a6 --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/Crashlytics/Crashlytics.framework/Resources b/Crashlytics/Crashlytics.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/Crashlytics/Crashlytics.framework/Versions/A/Crashlytics b/Crashlytics/Crashlytics.framework/Versions/A/Crashlytics new file mode 100644 index 00000000..b8def399 Binary files /dev/null and b/Crashlytics/Crashlytics.framework/Versions/A/Crashlytics differ diff --git a/Crashlytics/Crashlytics.framework/Versions/A/Headers/Crashlytics.h b/Crashlytics/Crashlytics.framework/Versions/A/Headers/Crashlytics.h new file mode 100644 index 00000000..fa37983c --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Versions/A/Headers/Crashlytics.h @@ -0,0 +1,89 @@ +// +// Crashlytics.h +// Crashlytics +// +// Copyright 2012 Crashlytics, Inc. All rights reserved. +// + +#import + +@protocol CrashlyticsDelegate; + +@interface Crashlytics : NSObject { +@private + NSString *_apiKey; + NSString *_dataDirectory; + NSString *_bundleIdentifier; + BOOL _installed; + NSMutableDictionary *_customAttributes; + id _user; + NSInteger _sendButtonIndex; + NSInteger _alwaysSendButtonIndex; + NSObject *_delegate; +} + +@property (nonatomic, readonly, copy) NSString *apiKey; +@property (nonatomic, readonly, copy) NSString *version; +@property (nonatomic, assign) BOOL debugMode; + +@property (nonatomic, assign) NSObject *delegate; + +/** + * + * The recommended way to install Crashlytics into your application is to place a call + * to +startWithAPIKey: in your -application:didFinishLaunchingWithOptions: method. + * + * This delay defaults to 1 second in order to generally give the application time to + * fully finish launching. + * + **/ ++ (Crashlytics *)startWithAPIKey:(NSString *)apiKey; ++ (Crashlytics *)startWithAPIKey:(NSString *)apiKey afterDelay:(NSTimeInterval)delay; + +/** + * + * If you need the functionality provided by the CrashlyticsDelegate protocol, you can use + * these convenience methods to activate the framework and set the delegate in one call. + * + **/ ++ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(NSObject *)delegate; ++ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(NSObject *)delegate afterDelay:(NSTimeInterval)delay; + +/** + * + * Access the singleton Crashlytics instance. + * + **/ ++ (Crashlytics *)sharedInstance; + +/** + * + * The easiest way to cause a crash - great for testing! + * + **/ +- (void)crash; + +@end + +/** + * + * The CrashlyticsDelegate protocol provides a mechanism for your application to take + * action on events that occur in the Crashlytics crash reporting system. You can make + * use of these calls by assigning an object to the Crashlytics' delegate property directly, + * or through the convenience startWithAPIKey:delegate:... methods. + * + **/ +@protocol CrashlyticsDelegate +@optional + +/** + * + * Called once a Crashlytics instance has determined that the last execution of the + * application ended in a crash. This is called some time after the crash reporting + * process has begun. If you have specififed a delay in one of the + * startWithAPIKey:... calls, this will take at least that long to be invoked. + * + **/ +- (void)crashlyticsDidDetectCrashDuringPreviousExecution:(Crashlytics *)crashlytics; + +@end diff --git a/Crashlytics/Crashlytics.framework/Versions/A/Resources/Info.plist b/Crashlytics/Crashlytics.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..f144fde6 --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,54 @@ + + + + + BuildMachineOSBuild + 11C74 + CFBundleDevelopmentRegion + English + CFBundleExecutable + Crashlytics + CFBundleIdentifier + com.crashlytics.ios + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Crashlytics + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.1.3 + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + iPhoneOS + + CFBundleVersion + 0100.01.03 + CrashlyticsAPIKey + 0d10c90776f5ef5acd01ddbeaca9a6cba4814560 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 8H7 + DTPlatformName + iphoneos + DTPlatformVersion + 4.3 + DTSDKBuild + 8H7 + DTSDKName + iphoneos4.3 + DTXcode + 0410 + DTXcodeBuild + 4B110 + MinimumOSVersion + 3.1 + UIDeviceFamily + + 1 + + + diff --git a/Crashlytics/Crashlytics.framework/Versions/A/Resources/Runner b/Crashlytics/Crashlytics.framework/Versions/A/Resources/Runner new file mode 120000 index 00000000..faa4f129 --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Versions/A/Resources/Runner @@ -0,0 +1 @@ +../../../run \ No newline at end of file diff --git a/Crashlytics/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings b/Crashlytics/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..3967e063 Binary files /dev/null and b/Crashlytics/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings differ diff --git a/Crashlytics/Crashlytics.framework/Versions/A/Resources/runner.rb b/Crashlytics/Crashlytics.framework/Versions/A/Resources/runner.rb new file mode 100644 index 00000000..b6ea201c --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Versions/A/Resources/runner.rb @@ -0,0 +1,15 @@ +#!/usr/bin/ruby + +# +# WARNING: DO NOT MODIFY THIS FILE. +# +# Crashlytics +# Crashlytics Version: 1.0.0.1 +# +# Copyright Crashlytics, Inc. 2012. All rights reserved. +# + +require 'pathname' + +path = Pathname.new(__FILE__).parent +`#{path}/../../../run` \ No newline at end of file diff --git a/Crashlytics/Crashlytics.framework/Versions/Current b/Crashlytics/Crashlytics.framework/Versions/Current new file mode 120000 index 00000000..8c7e5a66 --- /dev/null +++ b/Crashlytics/Crashlytics.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/Crashlytics/Crashlytics.framework/run b/Crashlytics/Crashlytics.framework/run new file mode 100755 index 00000000..7506e555 Binary files /dev/null and b/Crashlytics/Crashlytics.framework/run differ diff --git a/Crashlytics/Crashlytics.plist b/Crashlytics/Crashlytics.plist new file mode 100644 index 00000000..efa55526 --- /dev/null +++ b/Crashlytics/Crashlytics.plist @@ -0,0 +1,8 @@ + + + + + API Key + + + diff --git a/External/Pearl b/External/Pearl index 9b1a79dc..32660d8e 160000 --- a/External/Pearl +++ b/External/Pearl @@ -1 +1 @@ -Subproject commit 9b1a79dc981997c2c0c0276bca4aad2eeb9de9cd +Subproject commit 32660d8ebc2701bf8f702c64c1bdc1b9382b1612 diff --git a/Localytics/Localytics.plist b/Localytics/Localytics.plist new file mode 100644 index 00000000..24012ab5 --- /dev/null +++ b/Localytics/Localytics.plist @@ -0,0 +1,10 @@ + + + + + Key.development + e6238ceba8ec92832e77b1b-9ccd60bc-c39b-11e0-06e4-007f58cb3154 + Key.distribution + + + diff --git a/Localytics/LocalyticsDatabase.h b/Localytics/LocalyticsDatabase.h new file mode 100644 index 00000000..9a1e1d58 --- /dev/null +++ b/Localytics/LocalyticsDatabase.h @@ -0,0 +1,57 @@ +// +// LocalyticsDatabase.h +// LocalyticsDemo +// +// Created by jkaufman on 5/26/11. +// Copyright 2011 Localytics. All rights reserved. +// + +#import +#import + +#define MAX_DATABASE_SIZE 500000 // The maximum allowed disk size of the primary database file at open, in bytes +#define VACUUM_THRESHOLD 0.8 // The database is vacuumed after its size exceeds this proportion of the maximum. + +@interface LocalyticsDatabase : NSObject { + sqlite3 *_databaseConnection; +} + ++ (LocalyticsDatabase *)sharedLocalyticsDatabase; + +- (NSUInteger)databaseSize; +- (int)eventCount; +- (NSTimeInterval)createdTimestamp; + +- (BOOL)beginTransaction:(NSString *)name; +- (BOOL)releaseTransaction:(NSString *)name; +- (BOOL)rollbackTransaction:(NSString *)name; + +- (BOOL)incrementLastUploadNumber:(int *)uploadNumber; +- (BOOL)incrementLastSessionNumber:(int *)sessionNumber; + +- (BOOL)addEventWithBlobString:(NSString *)blob; +- (BOOL)addCloseEventWithBlobString:(NSString *)blob; +- (BOOL)addFlowEventWithBlobString:(NSString *)blob; +- (BOOL)removeLastCloseAndFlowEvents; + +- (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId; +- (int)unstagedEventCount; +- (BOOL)stageEventsForUpload:(sqlite3_int64)headerId; +- (BOOL)updateAppKey:(NSString *)appKey; +- (NSString *)uploadBlobString; +- (BOOL)deleteUploadedData; +- (BOOL)resetAnalyticsData; +- (BOOL)vacuumIfRequired; + +- (NSTimeInterval)lastSessionStartTimestamp; +- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp; + +- (BOOL)isOptedOut; +- (BOOL)setOptedOut:(BOOL)optOut; +- (NSString *)installId; +- (NSString *)appKey; // Most recent app key-- may not be that used to open the session. + +- (NSString *)customDimension:(int)dimension; +- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value; + +@end diff --git a/Localytics/LocalyticsDatabase.m b/Localytics/LocalyticsDatabase.m new file mode 100644 index 00000000..3b3700fe --- /dev/null +++ b/Localytics/LocalyticsDatabase.m @@ -0,0 +1,743 @@ +// +// LocalyticsDatabase.m +// LocalyticsDemo +// +// Created by jkaufman on 5/26/11. +// Copyright 2011 Localytics. All rights reserved. +// + +#import "LocalyticsDatabase.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)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]; +} + ++ (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; +} + +- (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); + } + + // 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]; + } + } + + return self; +} + +#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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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 *)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 +- (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]; + } +} + +- (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); + } +} + +// 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); + } +} + +// V3 adds a field for the last app key and patches a V2 migration issue. +- (void)upgradeToSchemaV3 { + sqlite3_exec(_databaseConnection, + "ALTER TABLE localytics_info ADD app_key CHAR(64)", + NULL, NULL, NULL); +} + +- (NSUInteger)databaseSize { + NSUInteger 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)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) == 1; + } + 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)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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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; +} + +- (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]; +} + +- (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; +} + +- (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,"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", + 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; +} + +- (NSString *)randomUUID { + CFUUIDRef theUUID = CFUUIDCreate(NULL); + CFStringRef stringUUID = CFUUIDCreateString(NULL, theUUID); + CFRelease(theUUID); + return [(NSString *)stringUUID autorelease]; +} + +#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 { + return self; +} + +- (id)retain { + return self; +} + +- (unsigned)retainCount { + // maximum value of an unsigned int - prevents additional retains for the class + return UINT_MAX; +} + +- (oneway void)release { + // ignore release commands +} + +- (id)autorelease { + return self; +} + +- (void)dealloc { + sqlite3_close(_databaseConnection); + [super dealloc]; +} + +@end diff --git a/Localytics/LocalyticsSession.h b/Localytics/LocalyticsSession.h new file mode 100644 index 00000000..e204469d --- /dev/null +++ b/Localytics/LocalyticsSession.h @@ -0,0 +1,216 @@ +// LocalyticsSession.h +// Copyright (C) 2009 Char Software Inc., DBA Localytics +// +// This code is provided under the Localytics Modified BSD License. +// A copy of this license has been distributed in a file called LICENSE +// with this source code. +// +// Please visit www.localytics.com for more information. + +#import + +// Set this to true to enable localytics traces (useful for debugging) +#define DO_LOCALYTICS_LOGGING false + +/*! + @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.
  • +
  • Open your session and begin your uploads in applicationDidFinishLaunching. This way the + upload has time to complete and it all happens before your users have a + chance to begin any data intensive actions of their own.
  • +
  • Close the session in applicationWillTerminate, and in applicationDidEnterBackground.
  • +
  • Resume the session in applicationWillEnterForeground.
  • +
  • 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 + multiple application keys. This can cause invalid state.
  • +
+ + @author Localytics + */ +@interface LocalyticsSession : NSObject { + + 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. +} + +@property dispatch_queue_t queue; +@property dispatch_group_t criticalGroup; +@property BOOL isSessionOpen; +@property BOOL hasInitialized; +@property float backgroundSessionTimeout; + +#pragma mark Public Methods +/*! + @method sharedLocalyticsSession + @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] + So, to tag an event, all that is necessary, anywhere in the code is: + [[LocalyticsSession sharedLocalyticsSession] tagEvent:@"MY_EVENT"]; + */ ++ (LocalyticsSession *)sharedLocalyticsSession; + +/*! + @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, + this method fascilitates that behavior. + It is recommended that this call be placed in applicationDidFinishLaunching. + @param applicationKey The key unique for each application generated + at www.localytics.com + */ +- (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 + 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. +
+ If for any reason this is called more than once every subsequent open call + will be ignored. + */ +- (void)open; + +/*! + @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 + 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 + is reopened. +*/ +- (void)resume; + +/*! + @method close + @abstract Closes the Localytics session. This should be called in + applicationWillTerminate. +
+ If close is not called, the session will still be uploaded but no + events will be processed and the session time will not appear. This is + because the session is not yet closed so it should not be used in + comparison with sessions which are closed. + */ +- (void)close; + +/*! + @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. + For another example, in a game with many levels it might be valuable + to create a new tag every time the user gets to a new level in order + to determine how far the average user is progressing in the game. +
+ Tagging Best Practices +
    +
  • DO NOT use tags to record personally identifiable information.
  • +
  • The best way to use tags is to create all the tag strings as predefined + constants and only use those. This is more efficient and removes the risk of + collecting personal information.
  • +
  • Do not set tags inside loops or any other place which gets called + frequently. This can cause a lot of data to be stored and uploaded.
  • +
+
+ See the tagging guide at: http://wiki.localytics.com/ + @param event The name of the event which occurred. + */ +- (void)tagEvent:(NSString *)event; + +- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes; + +- (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 + */ +- (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 + 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 setCustomDimension + @abstract (ENTERPRISE ONLY) 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. + 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. + */ +- (void)setCustomDimension:(int)dimension value:(NSString *)value; + +@end diff --git a/Localytics/LocalyticsSession.m b/Localytics/LocalyticsSession.m new file mode 100644 index 00000000..8f75c36d --- /dev/null +++ b/Localytics/LocalyticsSession.m @@ -0,0 +1,1148 @@ +// LocalyticsSession.m +// Copyright (C) 2009 Char Software Inc., DBA Localytics +// +// This code is provided under the Localytics Modified BSD License. +// A copy of this license has been distributed in a file called LICENSE +// with this source code. +// +// Please visit www.localytics.com for more information. + +#import "LocalyticsSession.h" +#import "WebserviceConstants.h" +#import "LocalyticsUploader.h" +#import "LocalyticsDatabase.h" + +#include +#include +#include +#include +#include +#include +#include + +#pragma mark Constants +#define PREFERENCES_KEY @"_localytics_install_id" // The randomly generated ID for each install of the app +#define CLIENT_VERSION @"iOS_2.6" // 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/" + +#define DEFAULT_BACKGROUND_SESSION_TIMEOUT 15 // Default value for how many seconds a session persists when App shifts to the background. + +// 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; + +// 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)logMessage:(NSString *)message; + +// Datapoint methods. +- (NSString *)customDimensions; +- (NSString *)macAddress; +- (NSString *)hashString:(NSString *)input; +- (NSString *)randomUUID; +- (NSString *)escapeString:(NSString *)input; +- (NSString *)installationId; +- (NSString *)uniqueDeviceIdentifier; +- (NSString *)appVersion; +- (NSTimeInterval)currentTimestamp; +- (BOOL)isDeviceJailbroken; +- (NSString *)deviceModel; +- (NSString *)modelSizeString; +- (double)availableMemory; + +@end + +@implementation LocalyticsSession + +@synthesize queue = _queue; +@synthesize criticalGroup = _criticalGroup; +@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 unstagedFlowEvents = _unstagedFlowEvents; +@synthesize stagedFlowEvents = _stagedFlowEvents; +@synthesize screens = _screens; +@synthesize sessionActiveDuration = _sessionActiveDuration; +@synthesize sessionHasBeenOpen = _sessionHasBeenOpen; + +#pragma mark Singleton + ++ (LocalyticsSession *)sharedLocalyticsSession { + @synchronized(self) { + if (_sharedLocalyticsSession == nil) { + _sharedLocalyticsSession = [[self alloc] init]; + } + } + return _sharedLocalyticsSession; +} + +- (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(); + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + + [LocalyticsDatabase sharedLocalyticsDatabase]; + } + + 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) + { + [self logMessage:@"Object has already been initialized."]; + 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) {} +} + +- (void)startSession:(NSString *)appKey { + [self LocalyticsSession:appKey]; + [self open]; + [self upload]; +} + +// Public interface to ll_open. +- (void)open { + dispatch_async(_queue, ^{ + [self ll_open]; + }); +} + +- (void)resume { + dispatch_async(_queue, ^{ + // Do nothing if session is already open + if(self.isSessionOpen == YES) + return; + + if([self ll_isOptedIn] == false) { + [self logMessage:@"Can't resume session because user is opted out."]; + return; + } + + // 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. + [self logMessage:@"Resume called - Resuming previous session."]; + [self reopenPreviousSession]; + } else { + // otherwise open new session and upload + [self logMessage:@"Resume called - Opening a new session."]; + [self ll_open]; + } + self.sessionCloseTime = nil; + }); +} + +- (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:@",\"%@\":%u", PARAM_SESSION_START, (long)self.lastSessionStartTimestamp]; + [closeEventString appendFormat:@",\"%@\":%u", PARAM_SESSION_ACTIVE, (long)self.sessionActiveDuration]; + [closeEventString appendFormat:@",\"%@\":%u", 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:@",\"%@\":%u", 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]]; + + // Close first level - close blob + [closeEventString appendString:@"}\n"]; + + BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] addCloseEventWithBlobString:[[closeEventString copy] autorelease]]; + + self.isSessionOpen = NO; // Session is no longer open. + + if (success) { + // Record final session flow, opting to remove it from the database if the session happens to resume. + // This is safe now that the session has closed because no new events can be added. + success = [self saveApplicationFlowAndRemoveOnResume:YES]; + } + + if (success) { + [self logMessage:@"Session succesfully closed."]; + } else { + [self logMessage:@"Failed to record session close."]; + } + } + @catch (NSException * e) {} + }); +} + +- (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) {} + }); +} + +// Public interface to ll_isOptedIn. +- (BOOL)isOptedIn { + __block BOOL optedIn = YES; + dispatch_sync(_queue, ^{ + optedIn = [self ll_isOptedIn]; + }); + return optedIn; +} + +// A convenience function for users who don't wish to add attributes. +- (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 { + [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:@",\"%@\":%u", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; + + // Append the custom dimensions + [eventString appendString:[self customDimensions]]; + + // 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 logMessage:[@"Tagged event: " stringByAppendingString:event]]; + } else { + [self logMessage:@"Failed to tag event."]; + } + } + @catch (NSException * e) {} + }); +} + +- (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)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)upload { + dispatch_group_async(_criticalGroup, _queue, ^{ + @try { + if ([[LocalyticsUploader sharedLocalyticsUploader] isUploading]) { + [self logMessage:@"An upload is already in progress. Aborting."]; + return; + } + + NSString *t = @"stage_upload"; + LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase]; + 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 && [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]; + } + + // 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. + [[LocalyticsUploader sharedLocalyticsUploader] uploaderWithApplicationKey:self.applicationKey]; + } else { + [db rollbackTransaction:t]; + [self logMessage:@"Failed to start upload."]; + } + } + @catch (NSException * e) { } + }); +} + +#pragma mark Private Methods + +- (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.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]; + + // 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]; + } + + 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:@",\"%@\":%u", PARAM_CLIENT_TIME, (long)self.lastSessionStartTimestamp]; + [openEventString appendFormat:@",\"%@\":%d", PARAM_SESSION_NUMBER, sessionNumber]; + + [openEventString appendString:[self customDimensions]]; + + [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) {} +} + +/*! + @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; +} + +/*! + @method addFlowEventWithName:type: + @abstract Adds a simple key-value pair to the list of events tagged during this session. + @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]; +} + +/*! + @method addScreenWithName: + @abstract Adds a name to list of screens encountered during this session. + @discussion The complete list of names is sent with the session close event. Screen names are stored in parallel to the + 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]]; +} + +/*! + @method blobHeaderStringWithSequenceNumber: + @abstract Creates the JSON string for the upload blob header, substituting in the given upload sequence number. + @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. + 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]; + NSString *locale_country = [english displayNameForKey:NSLocaleCountryCode value:[locale objectForKey:NSLocaleCountryCode]]; + NSString *uuid = [self randomUUID]; + NSString *device_uuid = [self uniqueDeviceIdentifier]; + + // Open first level - blob information + [headerString appendString:@"{"]; + [headerString appendFormat:@"\"%@\":%d", PARAM_SEQUENCE_NUMBER, nextSequenceNumber]; + [headerString appendFormat:@",\"%@\":%u", 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]]; + + // >> Application and session information + [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:CLIENT_VERSION ]]; + + // >> Device Information +// [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_UUID value:device_uuid ]]; + [headerString appendString:[self formatAttributeWithName:PARAM_DEVICE_UUID_HASHED value:[self hashString:device_uuid] ]]; + [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:@",\"%@\":%d", PARAM_DEVICE_MEMORY, (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"]; + + return [[headerString copy] autorelease]; +} + +- (BOOL)ll_isOptedIn { + return [[LocalyticsDatabase sharedLocalyticsDatabase] isOptedOut] == NO; +} + +/*! + @method createOptEvent: + @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 { + NSMutableString *optEventString = [NSMutableString string]; + [optEventString appendString:@"{"]; + [optEventString appendString:[self formatAttributeWithName:PARAM_DATA_TYPE value:@"o" first:YES]]; + [optEventString appendString:[self formatAttributeWithName:PARAM_APP_KEY value:self.applicationKey first:NO]]; + [optEventString appendString:[NSString stringWithFormat:@",\"%@\":%@", PARAM_OPT_VALUE, (optState ? @"true" : @"false") ]]; + [optEventString appendFormat:@",\"%@\":%u", PARAM_CLIENT_TIME, (long)[self currentTimestamp]]; + [optEventString appendString:@"}\n"]; + + BOOL success = [[LocalyticsDatabase sharedLocalyticsDatabase] addEventWithBlobString:[[optEventString copy] autorelease]]; + return success; +} + +/* + @method saveApplicationFlowAndRemoveOnResume: + @abstract Constructs an application flow blob string and writes it to the database, optionally flagging it for deletion + if the session is resumed. + @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:@",\"%@\":%u", 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; +} + +// 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]; +} + +/*! + @method formatAttributeWithName:value:firstAttribute: + @abstract Returns the given string key/value pair as a JSON string. + @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 { + // 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]; + return [[formattedString copy] autorelease]; +} + +/*! + @method escapeString + @abstract Formats the input string so it fits nicely in a JSON document. This includes + escaping double quote and slash characters. + @return The escaped version of the input string + */ +- (NSString *)escapeString:(NSString *)input +{ + NSString *output = [input stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + output = [output stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; + output = [output stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; + return output; +} + +- (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; + } + }); +} + +/*! + @method logMessage + @abstract Logs a message with (localytics) prepended to it. + @param message The message to log + */ +- (void)logMessage:(NSString *)message +{ + if(DO_LOCALYTICS_LOGGING) { + NSLog(@"(localytics) %s\n", [message UTF8String]); + } +} + +#pragma mark Datapoint Functions +/*! + @method customDimensions + @abstract Returns the json blob containing the custom dimensions. Assumes this will be appended + to an existing blob and as a result prepends the results with a comma. + */ +- (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]; +} + +/*! + @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; +} + +/*! + @method hashString + @abstract SHA1 Hashes a string + */ +- (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; +} + +/*! + @method randomUUID + @abstract Generates a random UUID + @return NSString containing the new UUID + */ +- (NSString *)randomUUID { + CFUUIDRef theUUID = CFUUIDCreate(NULL); + CFStringRef stringUUID = CFUUIDCreateString(NULL, theUUID); + CFRelease(theUUID); + return [(NSString *)stringUUID autorelease]; +} + +/*! + @method installationId + @abstract Looks in user preferences for an ID unique to this installation. If one is not + found it checks if one happens to be in the database (carroyover from older version of the db) + 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; +} + +/*! + @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 + be tied to a user account. [UIDevice Class Reference] + @return An 1-way hashed identifier unique to this device. + */ +- (NSString *)uniqueDeviceIdentifier { + +// Supress the warning for uniqueIdentifier being deprecated. +// 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. This will be removed before it causes grief. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSString *systemId = [[UIDevice currentDevice] uniqueIdentifier]; +#pragma clang diagnostic pop + + return systemId; +} + +/*! + @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"]; +} + +/*! + @method currentTimestamp + @abstract Gets the current time as seconds since Unix epoch. + @return an NSTimeInterval time. + */ +- (NSTimeInterval)currentTimestamp { + return [[NSDate date] timeIntervalSince1970]; +} + +/*! + @method isDeviceJailbroken + @abstract checks for the existance of apt to determine whether the user is running any + of the jailbroken app sources. + @return whether or not the device is jailbroken. + */ +- (BOOL) isDeviceJailbroken { + NSFileManager *sessionFileManager = [NSFileManager defaultManager]; + return [sessionFileManager fileExistsAtPath:PATH_TO_APT]; +} + +/*! + @method deviceModel + @abstract Gets the device model string. + @return a platform string identifying the device + */ +- (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 + 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 { + +#if TARGET_IPHONE_SIMULATOR + return @"simulator"; +#endif + + // User partition + NSArray *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + 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]; + uint64_t system = [[stats objectForKey:NSFileSystemSize] longLongValue]; + + // Add up and convert to gigabytes + // TODO: seem to be missing a system partiton or two... + NSInteger size = (user + system) >> 30; + + // Find nearest power of 2 (eg, 1,2,4,8,16,32,etc). Over 64 and we return 0 + for (NSInteger gig = 1; gig < 257; gig = gig << 1) { + if (size < gig) + return [NSString stringWithFormat:@"%dGB", gig]; + } + return nil; +} + +/*! + @method availableMemory + @abstract Reports how much memory is available + @return A double containing the available free memory + */ +- (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; +} + + +#pragma mark System Functions ++ (id)allocWithZone:(NSZone *)zone { + @synchronized(self) { + if (_sharedLocalyticsSession == nil) { + _sharedLocalyticsSession = [super allocWithZone:zone]; + return _sharedLocalyticsSession; + } + } + // returns nil on subsequent allocations + return nil; +} + +- (id)copyWithZone:(NSZone *)zone { + return self; +} + +- (id)retain { + return self; +} + +- (unsigned)retainCount { + // maximum value of an unsigned int - prevents additional retains for the class + return UINT_MAX; +} + +- (oneway void)release { + // ignore release commands +} + +- (id)autorelease { + return self; +} + +- (void)dealloc { + [[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]; + [_sharedLocalyticsSession release]; + + [super dealloc]; +} + +@end diff --git a/Localytics/LocalyticsUploader.h b/Localytics/LocalyticsUploader.h new file mode 100644 index 00000000..2d6b867e --- /dev/null +++ b/Localytics/LocalyticsUploader.h @@ -0,0 +1,42 @@ +// LocalyticsUploader.h +// Copyright (C) 2009 Char Software Inc., DBA Localytics +// +// This code is provided under the Localytics Modified BSD License. +// A copy of this license has been distributed in a file called LICENSE +// with this source code. +// +// Please visit www.localytics.com for more information. + +#import + +/*! + @class LocalyticsUploader + @discussion Singleton class to handle data uploads + */ + +@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; + +/*! + @method LocalyticsUploader + @abstract Creates a thread which uploads all queued header and event data. + All files starting with sessionFilePrefix are renamed, + uploaded and deleted on upload. This way the sessions can continue + writing data regardless of whether or not the upload succeeds. Files + which have been renamed still count towards the total number of Localytics + files which can be stored on the disk. + @param localyticsApplicationKey the Localytics application ID + */ +- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey; + +@end \ No newline at end of file diff --git a/Localytics/LocalyticsUploader.m b/Localytics/LocalyticsUploader.m new file mode 100644 index 00000000..251c8f97 --- /dev/null +++ b/Localytics/LocalyticsUploader.m @@ -0,0 +1,236 @@ +// LocalyticsUploader.m +// Copyright (C) 2009 Char Software Inc., DBA Localytics +// +// This code is provided under the Localytics Modified BSD License. +// A copy of this license has been distributed in a file called LICENSE +// with this source code. +// +// Please visit www.localytics.com for more information. + +#import "LocalyticsUploader.h" +#import "LocalyticsSession.h" +#import "LocalyticsDatabase.h" +#import + +#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads" + +static LocalyticsUploader *_sharedUploader = nil; + +@interface LocalyticsUploader () +- (void)finishUpload; +- (NSData *)gzipDeflatedDataWithData:(NSData *)data; +- (void)logMessage:(NSString *)message; + +@property (readwrite) BOOL isUploading; + +@end + +@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 { + + // Do nothing if already uploading. + if (self.isUploading == true) + { + [self logMessage:@"Upload already in progress. Aborting."]; + return; + } + + [self logMessage:@"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; + } + + NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding]; + NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease]; + [self logMessage:[NSString stringWithFormat:@"Uploading data (length: %u)", [myString length]]]; + + // Step 2 + NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain]; + + [pool drain]; + + NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString] + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:60.0]; + [submitRequest setHTTPMethod:@"POST"]; + [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; + [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]; + } + } + } + @catch (NSException * e) {} + + [self finishUpload]; + }); +} + +- (void)finishUpload +{ + self.isUploading = false; + + // Upload data has been deleted. Recover the disk space if necessary. + [[LocalyticsDatabase sharedLocalyticsDatabase] vacuumIfRequired]; +} + +/*! + @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. + @return the deflated data + */ +- (NSData *)gzipDeflatedDataWithData:(NSData *)data +{ + if ([data length] == 0) return data; + + z_stream strm; + + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.total_out = 0; + strm.next_in=(Bytef *)[data bytes]; + strm.avail_in = [data length]; + + // Compresssion Levels: + // Z_NO_COMPRESSION + // Z_BEST_SPEED + // Z_BEST_COMPRESSION + // Z_DEFAULT_COMPRESSION + + if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil; + + NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion + + do { + + if (strm.total_out >= [compressed length]) + [compressed increaseLengthBy: 16384]; + + strm.next_out = [compressed mutableBytes] + strm.total_out; + strm.avail_out = [compressed length] - strm.total_out; + + deflate(&strm, Z_FINISH); + + } while (strm.avail_out == 0); + + deflateEnd(&strm); + + [compressed setLength: strm.total_out]; + 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]); + } +} + +#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 { + return self; +} + +- (id)retain { + return self; +} + +- (unsigned)retainCount { + // maximum value of an unsigned int - prevents additional retains for the class + return UINT_MAX; +} + +- (oneway void)release { + // ignore release commands +} + +- (id)autorelease { + return self; +} + +- (void)dealloc { + [_sharedUploader release]; + [super dealloc]; +} + +@end diff --git a/Localytics/UploaderThread.h b/Localytics/UploaderThread.h new file mode 100644 index 00000000..0b59c5a4 --- /dev/null +++ b/Localytics/UploaderThread.h @@ -0,0 +1,48 @@ +// UploaderThread.h +// Copyright (C) 2009 Char Software Inc., DBA Localytics +// +// This code is provided under the Localytics Modified BSD License. +// A copy of this license has been distributed in a file called LICENSE +// with this source code. +// +// Please visit www.localytics.com for more information. + +#import + +/*! + @class UploaderThread + @discussion Singleton class to handle data uploads + */ + +@interface UploaderThread : NSObject { + NSURLConnection *_uploadConnection; // The connection which uploads the bits + NSInteger _responseStatusCode; // The HTTP response status code for the current connection + + BOOL _isUploading; // A flag to gaurantee only one uploader instance can happen at once +} + +@property (nonatomic, retain) NSURLConnection *uploadConnection; + +@property BOOL isUploading; + +/*! + @method sharedUploaderThread + @abstract Establishes this as a Singleton Class allowing for data persistence. + The class is accessed within the code using the following syntax: + [[UploaderThread sharedUploaderThread] functionHere] + */ ++ (UploaderThread *)sharedUploaderThread; + +/*! + @method UploaderThread + @abstract Creates a thread which uploads all queued header and event data. + All files starting with sessionFilePrefix are renamed, + uploaded and deleted on upload. This way the sessions can continue + writing data regardless of whether or not the upload succeeds. Files + which have been renamed still count towards the total number of Localytics + files which can be stored on the disk. + @param localyticsApplicationKey the Localytics application ID + */ +- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey; + +@end \ No newline at end of file diff --git a/Localytics/UploaderThread.m b/Localytics/UploaderThread.m new file mode 100644 index 00000000..3c04c127 --- /dev/null +++ b/Localytics/UploaderThread.m @@ -0,0 +1,260 @@ +// UploaderThread.m +// Copyright (C) 2009 Char Software Inc., DBA Localytics +// +// This code is provided under the Localytics Modified BSD License. +// A copy of this license has been distributed in a file called LICENSE +// with this source code. +// +// Please visit www.localytics.com for more information. + +#import "UploaderThread.h" +#import "LocalyticsSession.h" +#import "LocalyticsDatabase.h" +#import + +#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads" // url to send the + +static UploaderThread *_sharedUploaderThread = nil; + +@interface UploaderThread () +- (void)complete; +- (NSData *)gzipDeflatedDataWithData:(NSData *)data; +- (void)logMessage:(NSString *)message; +@end + +@implementation UploaderThread + +@synthesize uploadConnection = _uploadConnection; +@synthesize isUploading = _isUploading; + +#pragma mark Singleton Class ++ (UploaderThread *)sharedUploaderThread { + @synchronized(self) { + if (_sharedUploaderThread == nil) + { + _sharedUploaderThread = [[self alloc] init]; + } + } + return _sharedUploaderThread; +} + +#pragma mark Class Methods +- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey { + + // Do nothing if already uploading. + if (self.uploadConnection != nil || self.isUploading == true) + { + [self logMessage:@"Upload already in progress. Aborting."]; + return; + } + + [self logMessage:@"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 complete]; + return; + } + + NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding]; + NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease]; + [self logMessage:@"Upload data:"]; + [self logMessage:myString]; + + // Step 2 + NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain]; + + [pool drain]; + + NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString] + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:60.0]; + [submitRequest setHTTPMethod:@"POST"]; + [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]; + + // The NSURLConnection Object automatically spawns its own thread as a default behavior. + @try + { + [self logMessage:@"Spawning new thread for upload"]; + self.uploadConnection = [NSURLConnection connectionWithRequest:submitRequest delegate:self]; + + // Step 3 is handled by connectionDidFinishLoading. + } + @catch (NSException * e) + { + [self complete]; + } +} + +#pragma mark **** NSURLConnection FUNCTIONS **** + +- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { + // Used to gather response data from server - Not utilized in this version +} + +- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { + // Could receive multiple response callbacks, likely due to redirection. + // Record status and act only when connection completes load. + _responseStatusCode = [(NSHTTPURLResponse *)response statusCode]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)connection { + // If the connection finished loading, the files should be deleted. 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 + { + // The connection finished loading and uploaded data should be deleted. 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] deleteUploadData]; + } + + // Close upload session + [self complete]; +} + +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { + // 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: %s", + [error code], + [error localizedDescription]]]; + + [self complete]; +} + +/*! + @method complete + @abstract closes the upload connection and reports back to the session that the upload is complete + */ +- (void)complete { + _responseStatusCode = 0; + self.uploadConnection = nil; + self.isUploading = false; +} + +/*! + @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. + @return the deflated data + */ +- (NSData *)gzipDeflatedDataWithData:(NSData *)data +{ + if ([data length] == 0) return data; + + z_stream strm; + + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.total_out = 0; + strm.next_in=(Bytef *)[data bytes]; + strm.avail_in = [data length]; + + // Compresssion Levels: + // Z_NO_COMPRESSION + // Z_BEST_SPEED + // Z_BEST_COMPRESSION + // Z_DEFAULT_COMPRESSION + + if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil; + + NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion + + do { + + if (strm.total_out >= [compressed length]) + [compressed increaseLengthBy: 16384]; + + strm.next_out = [compressed mutableBytes] + strm.total_out; + strm.avail_out = [compressed length] - strm.total_out; + + deflate(&strm, Z_FINISH); + + } while (strm.avail_out == 0); + + deflateEnd(&strm); + + [compressed setLength: strm.total_out]; + 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]); + } +} + +#pragma mark System Functions ++ (id)allocWithZone:(NSZone *)zone { + @synchronized(self) { + if (_sharedUploaderThread == nil) { + _sharedUploaderThread = [super allocWithZone:zone]; + return _sharedUploaderThread; + } + } + // returns nil on subsequent allocations + return nil; +} + +- (id)copyWithZone:(NSZone *)zone { + return self; +} + +- (id)retain { + return self; +} + +- (unsigned)retainCount { + // maximum value of an unsigned int - prevents additional retains for the class + return UINT_MAX; +} + +- (oneway void)release { + // ignore release commands +} + +- (id)autorelease { + return self; +} + +- (void)dealloc { + [_uploadConnection release]; + [_sharedUploaderThread release]; + [super dealloc]; +} + +@end diff --git a/Localytics/WebserviceConstants.h b/Localytics/WebserviceConstants.h new file mode 100644 index 00000000..fafdf88f --- /dev/null +++ b/Localytics/WebserviceConstants.h @@ -0,0 +1,111 @@ +// WebserviceConstants.h +// Copyright (C) 2009 Char Software Inc., DBA Localytics +// +// This code is provided under the Localytics Modified BSD License. +// A copy of this license has been distributed in a file called LICENSE +// with this source code. +// +// Please visit www.localytics.com for more information. + +// 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. + +/********************* + * Shared Attributes * + *********************/ +#define PARAM_UUID @"u" // UUID for JSON document +#define PARAM_DATA_TYPE @"dt" // Data Type +#define PARAM_CLIENT_TIME @"ct" // Client Time, seconds from Unix epoch (int) +#define PARAM_LATITUDE @"lat" // Latitude - if available +#define PARAM_LONGITUDE @"lon" // Longitude - if available +#define PARAM_SESSION_UUID @"su" // UUID for an existing session +#define PARAM_NEW_SESSION_UUID @"u" // UUID for a new session +#define PARAM_ATTRIBUTES @"attrs" // Attributes (dictionary) + +/*************** + * Blob Header * + ***************/ + +// PARAM_UUID +// 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 + // 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) + +/**************************** + * Header Common Attributes * + ****************************/ + +// PARAM_DATA_TYPE +#define PARAM_APP_KEY @"au" // Localytics Application ID +#define PARAM_DEVICE_UUID @"du" // Device UUID +#define PARAM_DEVICE_UUID_HASHED @"udid" // Hashed version of the UUID +#define PARAM_DEVICE_MAC @"wmac" // Hashed version of the device Mac +#define PARAM_INSTALL_ID @"iu" // Install ID +#define PARAM_JAILBROKEN @"j" // Jailbroken (boolean) +#define PARAM_LIBRARY_VERSION @"lv" // Client Version +#define PARAM_APP_VERSION @"av" // Application Version +#define PARAM_DEVICE_PLATFORM @"dp" // Device Platform +#define PARAM_LOCALE_LANGUAGE @"dll" // Locale Language +#define PARAM_LOCALE_COUNTRY @"dlc" // Locale Country +#define PARAM_NETWORK_COUNTRY @"nc" // Network Country (iso code) // ???: Never used on iPhone. +#define PARAM_DEVICE_COUNTRY @"dc" // Device Country (iso code) +#define PARAM_DEVICE_MANUFACTURER @"dma" // Device Manufacturer // ???: Never used on iPhone. Used to be "Device Make". +#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_DATA_CONNECTION @"dac" // Data Connection Type // ???: Never used on iPhone. +#define PARAM_OPT_VALUE @"optin" // Opt In (boolean) +#define PARAM_DEVICE_MEMORY @"dmem" // Device Memory + +/***************** + * Session Start * + *****************/ + +// PARAM_UUID +// PARAM_DATA_TYPE => "s" for Start +// PARAM_CLIENT_TIME +#define PARAM_SESSION_NUMBER @"nth" // This is the nth session on the device, 1-indexed (int) + +/**************** + * Session Stop * + ****************/ + +// PARAM_UUID +// PARAM_DATA_TYPE => "c" for Close +// PARAM_CLIENT_TIME +// PARAM_LATITUDE +// PARAM_LONGITUDE +// PARAM_SESSION_UUID => UUID of session being closed +#define PARAM_SESSION_ACTIVE @"cta" // Active time in seconds (time app was active) +#define PARAM_SESSION_TOTAL @"ctl" // Total session length +#define PARAM_SESSION_SCREENFLOW @"fl" // Screens encountered during this session, in order + +/********************* + * Application Event * + *********************/ + +// PARAM_UUID +// PARAM_DATA_TYPE => "e" for Event +// PARAM_CLIENT_TIME +// PARAM_LATITUDE +// PARAM_LONGITUDE +// PARAM_SESSION_UUID => UUID of session event occured in +// 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 + +/******************** + * Application flow * + ********************/ + +// PARAM_UUID +// PARAM_DATA_TYPE => "f" for Flow +// 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 diff --git a/MasterPassword-iOS.xcodeproj/project.pbxproj b/MasterPassword-iOS.xcodeproj/project.pbxproj index 6b130164..bdf791cc 100644 --- a/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -56,7 +56,6 @@ DA95D5F614DF0B9F008D1B94 /* IASKPSTextFieldSpecifierViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA95D5CC14DF0691008D1B94 /* IASKPSTextFieldSpecifierViewCell.xib */; }; DA95D5F714DF0B9F008D1B94 /* IASKPSToggleSwitchSpecifierViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA95D5CD14DF0691008D1B94 /* IASKPSToggleSwitchSpecifierViewCell.xib */; }; DA95D5F814DF0B9F008D1B94 /* IASKSpecifierValuesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA95D5CE14DF0691008D1B94 /* IASKSpecifierValuesView.xib */; }; - DA95D60614DF3E67008D1B94 /* libTestFlight.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA95D60114DF3E67008D1B94 /* libTestFlight.a */; }; DAB8D45D15036BCF00CED3BC /* MasterPassword.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D43C15036BCF00CED3BC /* MasterPassword.xcdatamodeld */; }; DAB8D45E15036BCF00CED3BC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DAB8D43F15036BCF00CED3BC /* InfoPlist.strings */; }; DAB8D45F15036BCF00CED3BC /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D44115036BCF00CED3BC /* main.m */; }; @@ -712,9 +711,6 @@ DAB8D97A15036BF700CED3BC /* tip_location_wood.png in Resources */ = {isa = PBXBuildFile; fileRef = DAB8D6F815036BF600CED3BC /* tip_location_wood.png */; }; DAB8D97B15036BF700CED3BC /* tip_location_wood@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DAB8D6F915036BF600CED3BC /* tip_location_wood@2x.png */; }; DAB8D97C1503718B00CED3BC /* jquery-1.6.1.min.js in Resources */ = {isa = PBXBuildFile; fileRef = DAB8D6AB15036BF600CED3BC /* jquery-1.6.1.min.js */; }; - DABB9809150FF40100B05417 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; }; - DABB980F150FF40100B05417 /* SendToMac.m in Sources */ = {isa = PBXBuildFile; fileRef = DABB980E150FF40100B05417 /* SendToMac.m */; }; - DABB9814150FF5C100B05417 /* libSendToMac.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DABB9808150FF40100B05417 /* libSendToMac.a */; }; DABB981615100B4000B05417 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DABB981515100B4000B05417 /* SystemConfiguration.framework */; }; DAC6325E1486805C0075AEA5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; }; DAC6326D148680650075AEA5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; }; @@ -722,10 +718,29 @@ DAC63278148680740075AEA5 /* libjrswizzle.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAC6326C148680650075AEA5 /* libjrswizzle.a */; }; DAC6327B1486809A0075AEA5 /* JRSwizzle.h in Headers */ = {isa = PBXBuildFile; fileRef = DAC632791486809A0075AEA5 /* JRSwizzle.h */; }; DAC6327C1486809A0075AEA5 /* JRSwizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = DAC6327A1486809A0075AEA5 /* JRSwizzle.m */; }; - DAC6327F148680B10075AEA5 /* UIColor-Expanded.h in Headers */ = {isa = PBXBuildFile; fileRef = DAC6327D148680B10075AEA5 /* UIColor-Expanded.h */; }; - DAC63280148680B10075AEA5 /* UIColor-Expanded.m in Sources */ = {isa = PBXBuildFile; fileRef = DAC6327E148680B10075AEA5 /* UIColor-Expanded.m */; }; DAC632891486D9690075AEA5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAC632871486D95D0075AEA5 /* Security.framework */; }; DAC77CAE148291A600BCF976 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; }; + DAD3126715528C9C00A3F9ED /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAD3125F15528C9C00A3F9ED /* Crashlytics.framework */; }; + DAD3126815528C9C00A3F9ED /* Crashlytics.plist in Resources */ = {isa = PBXBuildFile; fileRef = DAD3126015528C9C00A3F9ED /* Crashlytics.plist */; }; + DAD3126915528C9C00A3F9ED /* libTestFlight.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAD3126215528C9C00A3F9ED /* libTestFlight.a */; }; + DAD3126C15528C9C00A3F9ED /* TestFlight.plist in Resources */ = {isa = PBXBuildFile; fileRef = DAD3126615528C9C00A3F9ED /* TestFlight.plist */; }; + DAD3127215528CD200A3F9ED /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; }; + DAD3128715528D0F00A3F9ED /* LocalyticsDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD3127E15528D0F00A3F9ED /* LocalyticsDatabase.h */; }; + DAD3128815528D0F00A3F9ED /* LocalyticsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD3127F15528D0F00A3F9ED /* LocalyticsDatabase.m */; }; + DAD3128915528D0F00A3F9ED /* LocalyticsSession.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD3128015528D0F00A3F9ED /* LocalyticsSession.h */; }; + DAD3128A15528D0F00A3F9ED /* LocalyticsSession.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD3128115528D0F00A3F9ED /* LocalyticsSession.m */; }; + DAD3128B15528D0F00A3F9ED /* LocalyticsUploader.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD3128215528D0F00A3F9ED /* LocalyticsUploader.h */; }; + DAD3128C15528D0F00A3F9ED /* LocalyticsUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD3128315528D0F00A3F9ED /* LocalyticsUploader.m */; }; + DAD3128D15528D0F00A3F9ED /* UploaderThread.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD3128415528D0F00A3F9ED /* UploaderThread.h */; }; + DAD3128E15528D0F00A3F9ED /* UploaderThread.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD3128515528D0F00A3F9ED /* UploaderThread.m */; }; + DAD3128F15528D0F00A3F9ED /* WebserviceConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD3128615528D0F00A3F9ED /* WebserviceConstants.h */; }; + DAD3129015528D1600A3F9ED /* Localytics.plist in Resources */ = {isa = PBXBuildFile; fileRef = DAD3127D15528D0F00A3F9ED /* Localytics.plist */; }; + DAD312BB1552977200A3F9ED /* UIColor+Expanded.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD312B71552977200A3F9ED /* UIColor+Expanded.h */; }; + DAD312BC1552977200A3F9ED /* UIColor+Expanded.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD312B81552977200A3F9ED /* UIColor+Expanded.m */; }; + DAD312BD1552977200A3F9ED /* UIColor+HSV.h in Headers */ = {isa = PBXBuildFile; fileRef = DAD312B91552977200A3F9ED /* UIColor+HSV.h */; }; + DAD312BE1552977200A3F9ED /* UIColor+HSV.m in Sources */ = {isa = PBXBuildFile; fileRef = DAD312BA1552977200A3F9ED /* UIColor+HSV.m */; }; + DAD312BF1552A1BD00A3F9ED /* libLocalytics.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAD3127115528CD200A3F9ED /* libLocalytics.a */; }; + DAD312C21552A22700A3F9ED /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = DAD312C01552A20800A3F9ED /* libsqlite3.dylib */; }; DAEBC45314F6364500987BF6 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAEBC45214F6364500987BF6 /* QuartzCore.framework */; }; 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 */; }; @@ -890,8 +905,6 @@ DA95D5CD14DF0691008D1B94 /* IASKPSToggleSwitchSpecifierViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IASKPSToggleSwitchSpecifierViewCell.xib; sourceTree = ""; }; DA95D5CE14DF0691008D1B94 /* IASKSpecifierValuesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IASKSpecifierValuesView.xib; sourceTree = ""; }; DA95D5F014DF0B1E008D1B94 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; - DA95D60114DF3E67008D1B94 /* libTestFlight.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libTestFlight.a; sourceTree = ""; }; - DA95D60414DF3E67008D1B94 /* TestFlight.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestFlight.h; sourceTree = ""; }; DAB8D43D15036BCF00CED3BC /* MasterPassword.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MasterPassword.xcdatamodel; sourceTree = ""; }; DAB8D44015036BCF00CED3BC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DAB8D44115036BCF00CED3BC /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -1560,7 +1573,6 @@ DAB8D6F715036BF600CED3BC /* tip_location_teal@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tip_location_teal@2x.png"; sourceTree = ""; }; DAB8D6F815036BF600CED3BC /* tip_location_wood.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = tip_location_wood.png; sourceTree = ""; }; DAB8D6F915036BF600CED3BC /* tip_location_wood@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "tip_location_wood@2x.png"; sourceTree = ""; }; - DABB9808150FF40100B05417 /* libSendToMac.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSendToMac.a; sourceTree = BUILT_PRODUCTS_DIR; }; DABB980C150FF40100B05417 /* SendToMac-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SendToMac-Prefix.pch"; sourceTree = ""; }; DABB980D150FF40100B05417 /* SendToMac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SendToMac.h; sourceTree = ""; }; DABB980E150FF40100B05417 /* SendToMac.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SendToMac.m; sourceTree = ""; }; @@ -1569,11 +1581,30 @@ DAC6326C148680650075AEA5 /* libjrswizzle.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libjrswizzle.a; sourceTree = BUILT_PRODUCTS_DIR; }; DAC632791486809A0075AEA5 /* JRSwizzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = JRSwizzle.h; path = External/Pearl/External/jrswizzle/JRSwizzle.h; sourceTree = SOURCE_ROOT; }; DAC6327A1486809A0075AEA5 /* JRSwizzle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = JRSwizzle.m; path = External/Pearl/External/jrswizzle/JRSwizzle.m; sourceTree = SOURCE_ROOT; }; - DAC6327D148680B10075AEA5 /* UIColor-Expanded.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIColor-Expanded.h"; path = "External/Pearl/External/uicolor-utilities/UIColor-Expanded.h"; sourceTree = SOURCE_ROOT; }; - DAC6327E148680B10075AEA5 /* UIColor-Expanded.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIColor-Expanded.m"; path = "External/Pearl/External/uicolor-utilities/UIColor-Expanded.m"; sourceTree = SOURCE_ROOT; }; DAC632871486D95D0075AEA5 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; DAC77CAD148291A600BCF976 /* libPearl.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPearl.a; sourceTree = BUILT_PRODUCTS_DIR; }; DAC77CB1148291A600BCF976 /* Pearl-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Pearl-Prefix.pch"; sourceTree = ""; }; + DAD3125F15528C9C00A3F9ED /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Crashlytics.framework; sourceTree = ""; }; + DAD3126015528C9C00A3F9ED /* Crashlytics.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Crashlytics.plist; sourceTree = ""; }; + DAD3126215528C9C00A3F9ED /* libTestFlight.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libTestFlight.a; sourceTree = ""; }; + DAD3126515528C9C00A3F9ED /* TestFlight.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestFlight.h; sourceTree = ""; }; + DAD3126615528C9C00A3F9ED /* TestFlight.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TestFlight.plist; sourceTree = ""; }; + DAD3127115528CD200A3F9ED /* libLocalytics.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libLocalytics.a; sourceTree = BUILT_PRODUCTS_DIR; }; + DAD3127D15528D0F00A3F9ED /* Localytics.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Localytics.plist; sourceTree = ""; }; + DAD3127E15528D0F00A3F9ED /* LocalyticsDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsDatabase.h; sourceTree = ""; }; + DAD3127F15528D0F00A3F9ED /* LocalyticsDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsDatabase.m; sourceTree = ""; }; + DAD3128015528D0F00A3F9ED /* LocalyticsSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsSession.h; sourceTree = ""; }; + DAD3128115528D0F00A3F9ED /* LocalyticsSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsSession.m; sourceTree = ""; }; + DAD3128215528D0F00A3F9ED /* LocalyticsUploader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LocalyticsUploader.h; sourceTree = ""; }; + DAD3128315528D0F00A3F9ED /* LocalyticsUploader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalyticsUploader.m; sourceTree = ""; }; + DAD3128415528D0F00A3F9ED /* UploaderThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UploaderThread.h; sourceTree = ""; }; + DAD3128515528D0F00A3F9ED /* UploaderThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UploaderThread.m; sourceTree = ""; }; + DAD3128615528D0F00A3F9ED /* WebserviceConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebserviceConstants.h; sourceTree = ""; }; + DAD312B71552977200A3F9ED /* UIColor+Expanded.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIColor+Expanded.h"; path = "External/Pearl/External/uicolor-utilities/UIColor+Expanded.h"; sourceTree = SOURCE_ROOT; }; + DAD312B81552977200A3F9ED /* UIColor+Expanded.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIColor+Expanded.m"; path = "External/Pearl/External/uicolor-utilities/UIColor+Expanded.m"; sourceTree = SOURCE_ROOT; }; + DAD312B91552977200A3F9ED /* UIColor+HSV.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIColor+HSV.h"; path = "External/Pearl/External/uicolor-utilities/UIColor+HSV.h"; sourceTree = SOURCE_ROOT; }; + DAD312BA1552977200A3F9ED /* UIColor+HSV.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIColor+HSV.m"; path = "External/Pearl/External/uicolor-utilities/UIColor+HSV.m"; sourceTree = SOURCE_ROOT; }; + DAD312C01552A20800A3F9ED /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; DAEBC45214F6364500987BF6 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 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 = ""; }; @@ -1657,8 +1688,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DAD312C21552A22700A3F9ED /* libsqlite3.dylib in Frameworks */, + DAD312BF1552A1BD00A3F9ED /* libLocalytics.a in Frameworks */, DABB981615100B4000B05417 /* SystemConfiguration.framework in Frameworks */, - DABB9814150FF5C100B05417 /* libSendToMac.a in Frameworks */, DA672D3014F9413D004A189C /* libPearl.a in Frameworks */, DA672D2F14F92C6B004A189C /* libz.dylib in Frameworks */, DAEBC45314F6364500987BF6 /* QuartzCore.framework in Frameworks */, @@ -1670,7 +1702,8 @@ DA5BFA4B147E415C00F98B1E /* Foundation.framework in Frameworks */, DA5BFA4D147E415C00F98B1E /* CoreGraphics.framework in Frameworks */, DA5BFA4F147E415C00F98B1E /* CoreData.framework in Frameworks */, - DA95D60614DF3E67008D1B94 /* libTestFlight.a in Frameworks */, + DAD3126715528C9C00A3F9ED /* Crashlytics.framework in Frameworks */, + DAD3126915528C9C00A3F9ED /* libTestFlight.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1682,14 +1715,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DABB9805150FF40100B05417 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DABB9809150FF40100B05417 /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DAC6325A1486805C0075AEA5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1717,6 +1742,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DAD3126E15528CD200A3F9ED /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DAD3127215528CD200A3F9ED /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -1741,7 +1774,9 @@ DAC6325F1486805C0075AEA5 /* uicolor-utilities */, DAC6326E148680650075AEA5 /* jrswizzle */, DA95D59E14DF063C008D1B94 /* InAppSettingsKit */, - DA95D5FF14DF3E67008D1B94 /* TestFlightSDK */, + DAD3125E15528C9C00A3F9ED /* Crashlytics */, + DAD3126115528C9C00A3F9ED /* TestFlight */, + DAD3127315528CD200A3F9ED /* Localytics */, DA5BFA47147E415C00F98B1E /* Frameworks */, DA5BFA45147E415C00F98B1E /* Products */, ); @@ -1755,7 +1790,7 @@ DAC6325D1486805C0075AEA5 /* libuicolor-utilities.a */, DAC6326C148680650075AEA5 /* libjrswizzle.a */, DA95D59C14DF063C008D1B94 /* libInAppSettingsKit.a */, - DABB9808150FF40100B05417 /* libSendToMac.a */, + DAD3127115528CD200A3F9ED /* libLocalytics.a */, ); name = Products; sourceTree = ""; @@ -1763,6 +1798,7 @@ DA5BFA47147E415C00F98B1E /* Frameworks */ = { isa = PBXGroup; children = ( + DAD312C01552A20800A3F9ED /* libsqlite3.dylib */, DA95D5F014DF0B1E008D1B94 /* MessageUI.framework */, DABB981515100B4000B05417 /* SystemConfiguration.framework */, DA672D2E14F92C6B004A189C /* libz.dylib */, @@ -1787,10 +1823,10 @@ DA600C2615056427008E9AB6 /* MPConfig.h */, DA600C2715056427008E9AB6 /* MPConfig.m */, DAB8D45515036BCF00CED3BC /* MPElementStoredEntity.m */, + DAB8D45915036BCF00CED3BC /* MPTypes.h */, DAB8D45615036BCF00CED3BC /* MPTypes.m */, DAB8D45715036BCF00CED3BC /* MPElementEntity.h */, DAB8D45815036BCF00CED3BC /* MPElementEntity.m */, - DAB8D45915036BCF00CED3BC /* MPTypes.h */, DAB8D45A15036BCF00CED3BC /* MPElementGeneratedEntity.h */, DAB8D45B15036BCF00CED3BC /* MPElementGeneratedEntity.m */, DAB8D45C15036BCF00CED3BC /* MPElementStoredEntity.h */, @@ -1877,16 +1913,6 @@ path = External/InAppSettingsKit/InAppSettingsKit/Xibs; sourceTree = SOURCE_ROOT; }; - DA95D5FF14DF3E67008D1B94 /* TestFlightSDK */ = { - isa = PBXGroup; - children = ( - DA95D60114DF3E67008D1B94 /* libTestFlight.a */, - DA95D60414DF3E67008D1B94 /* TestFlight.h */, - ); - name = TestFlightSDK; - path = External/TestFlightSDK; - sourceTree = ""; - }; DAB8D43E15036BCF00CED3BC /* iOS */ = { isa = PBXGroup; children = ( @@ -2653,8 +2679,10 @@ DAC6325F1486805C0075AEA5 /* uicolor-utilities */ = { isa = PBXGroup; children = ( - DAC6327D148680B10075AEA5 /* UIColor-Expanded.h */, - DAC6327E148680B10075AEA5 /* UIColor-Expanded.m */, + DAD312B71552977200A3F9ED /* UIColor+Expanded.h */, + DAD312B81552977200A3F9ED /* UIColor+Expanded.m */, + DAD312B91552977200A3F9ED /* UIColor+HSV.h */, + DAD312BA1552977200A3F9ED /* UIColor+HSV.m */, ); path = "uicolor-utilities"; sourceTree = ""; @@ -2687,6 +2715,42 @@ name = "Supporting Files"; sourceTree = ""; }; + DAD3125E15528C9C00A3F9ED /* Crashlytics */ = { + isa = PBXGroup; + children = ( + DAD3125F15528C9C00A3F9ED /* Crashlytics.framework */, + DAD3126015528C9C00A3F9ED /* Crashlytics.plist */, + ); + path = Crashlytics; + sourceTree = ""; + }; + DAD3126115528C9C00A3F9ED /* TestFlight */ = { + isa = PBXGroup; + children = ( + DAD3126215528C9C00A3F9ED /* libTestFlight.a */, + DAD3126515528C9C00A3F9ED /* TestFlight.h */, + DAD3126615528C9C00A3F9ED /* TestFlight.plist */, + ); + path = TestFlight; + sourceTree = ""; + }; + DAD3127315528CD200A3F9ED /* Localytics */ = { + isa = PBXGroup; + children = ( + DAD3127D15528D0F00A3F9ED /* Localytics.plist */, + DAD3127E15528D0F00A3F9ED /* LocalyticsDatabase.h */, + DAD3127F15528D0F00A3F9ED /* LocalyticsDatabase.m */, + DAD3128015528D0F00A3F9ED /* LocalyticsSession.h */, + DAD3128115528D0F00A3F9ED /* LocalyticsSession.m */, + DAD3128215528D0F00A3F9ED /* LocalyticsUploader.h */, + DAD3128315528D0F00A3F9ED /* LocalyticsUploader.m */, + DAD3128415528D0F00A3F9ED /* UploaderThread.h */, + DAD3128515528D0F00A3F9ED /* UploaderThread.m */, + DAD3128615528D0F00A3F9ED /* WebserviceConstants.h */, + ); + path = Localytics; + sourceTree = ""; + }; DAFE45D715039823003ABA7C /* Pearl */ = { isa = PBXGroup; children = ( @@ -2827,18 +2891,12 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DABB9806150FF40100B05417 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DAC6325B1486805C0075AEA5 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - DAC6327F148680B10075AEA5 /* UIColor-Expanded.h in Headers */, + DAD312BB1552977200A3F9ED /* UIColor+Expanded.h in Headers */, + DAD312BD1552977200A3F9ED /* UIColor+HSV.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2895,6 +2953,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DAD3126F15528CD200A3F9ED /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DAD3128715528D0F00A3F9ED /* LocalyticsDatabase.h in Headers */, + DAD3128915528D0F00A3F9ED /* LocalyticsSession.h in Headers */, + DAD3128B15528D0F00A3F9ED /* LocalyticsUploader.h in Headers */, + DAD3128D15528D0F00A3F9ED /* UploaderThread.h in Headers */, + DAD3128F15528D0F00A3F9ED /* WebserviceConstants.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -2905,7 +2975,8 @@ DA5BFA40147E415C00F98B1E /* Sources */, DA5BFA41147E415C00F98B1E /* Frameworks */, DA5BFA42147E415C00F98B1E /* Resources */, - DA6556E314D55F3000841C99 /* ShellScript */, + DA6556E314D55F3000841C99 /* Run Script: GIT version -> Info.plist */, + DAD3125D155288AA00A3F9ED /* ShellScript */, ); buildRules = ( ); @@ -2933,23 +3004,6 @@ productReference = DA95D59C14DF063C008D1B94 /* libInAppSettingsKit.a */; productType = "com.apple.product-type.library.static"; }; - DABB9807150FF40100B05417 /* SendToMac */ = { - isa = PBXNativeTarget; - buildConfigurationList = DABB9810150FF40100B05417 /* Build configuration list for PBXNativeTarget "SendToMac" */; - buildPhases = ( - DABB9804150FF40100B05417 /* Sources */, - DABB9805150FF40100B05417 /* Frameworks */, - DABB9806150FF40100B05417 /* Headers */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = SendToMac; - productName = SendToMac; - productReference = DABB9808150FF40100B05417 /* libSendToMac.a */; - productType = "com.apple.product-type.library.static"; - }; DAC6325C1486805C0075AEA5 /* uicolor-utilities */ = { isa = PBXNativeTarget; buildConfigurationList = DAC632651486805C0075AEA5 /* Build configuration list for PBXNativeTarget "uicolor-utilities" */; @@ -3003,6 +3057,23 @@ productReference = DAC77CAD148291A600BCF976 /* libPearl.a */; productType = "com.apple.product-type.library.static"; }; + DAD3127015528CD200A3F9ED /* Localytics */ = { + isa = PBXNativeTarget; + buildConfigurationList = DAD3127915528CD200A3F9ED /* Build configuration list for PBXNativeTarget "Localytics" */; + buildPhases = ( + DAD3126D15528CD200A3F9ED /* Sources */, + DAD3126E15528CD200A3F9ED /* Frameworks */, + DAD3126F15528CD200A3F9ED /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Localytics; + productName = Localytics; + productReference = DAD3127115528CD200A3F9ED /* libLocalytics.a */; + productType = "com.apple.product-type.library.static"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -3037,7 +3108,7 @@ DAC6325C1486805C0075AEA5 /* uicolor-utilities */, DAC6326B148680650075AEA5 /* jrswizzle */, DA95D59B14DF063C008D1B94 /* InAppSettingsKit */, - DABB9807150FF40100B05417 /* SendToMac */, + DAD3127015528CD200A3F9ED /* Localytics */, ); }; /* End PBXProject section */ @@ -3720,13 +3791,31 @@ DAB8D97A15036BF700CED3BC /* tip_location_wood.png in Resources */, DAB8D97B15036BF700CED3BC /* tip_location_wood@2x.png in Resources */, DAFE4A5A1503982E003ABA7C /* Pearl.strings in Resources */, + DAD3126815528C9C00A3F9ED /* Crashlytics.plist in Resources */, + DAD3126C15528C9C00A3F9ED /* TestFlight.plist in Resources */, + DAD3129015528D1600A3F9ED /* Localytics.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - DA6556E314D55F3000841C99 /* ShellScript */ = { + DA6556E314D55F3000841C99 /* Run Script: GIT version -> Info.plist */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script: GIT version -> Info.plist"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = "/bin/bash -e"; + shellScript = "PATH+=:/usr/libexec\n\nsetPlistWithKey() {\n local key=$1 value=$2 plist=${3:-\"$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH\"}\n \n PlistBuddy -c \"Set :'$key' $value\" \"$plist\"\n}\ngetPlistWithKey() {\n local key=$1 plist=${2:-\"$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH\"}\n \n PlistBuddy -c \"Print :'$key'\" \"$plist\"\n}\nsetSettingWithTitle() {\n local i title=$1 value=$2 plist=${3:-\"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Settings.bundle/Root.plist\"}\n \n for (( i=0; 1; ++i )); do\n PlistBuddy -c \"Print :PreferenceSpecifiers:$i\" \"$plist\" &>/dev/null || break\n echo \"Checking preference specifier $i\"\n \n [[ $(PlistBuddy -c \"Print :PreferenceSpecifiers:$i:Title\" \"$plist\" 2>/dev/null) = $title ]] || continue\n \n echo \"Correct title, setting value.\"\n PlistBuddy -c \"Set :PreferenceSpecifiers:$i:DefaultValue $value\" \"$plist\"\n break\n done\n}\n\nbuild=$(git describe --tags --always --dirty --long)\ntag=$(git describe --tags | sed 's/-\\([^-]*\\)-[^-]*$/.\\1/')\n\nsetPlistWithKey CFBundleVersion \"$build\"\nsetPlistWithKey CFBundleShortVersionString \"$tag\"\n\nsetSettingWithTitle \"Build\" \"$build\"\nsetSettingWithTitle \"Version\" \"$tag\"\nsetSettingWithTitle \"Copyright\" \"$(getPlistWithKey NSHumanReadableCopyright)\"\n"; + showEnvVarsInLog = 0; + }; + DAD3125D155288AA00A3F9ED /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -3737,7 +3826,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = "/bin/bash -e"; - shellScript = "PATH+=:/usr/libexec\n\nsetPlistWithKey() {\n local key=$1 value=$2 plist=${3:-\"$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH\"}\n\n PlistBuddy -c \"Set :$key $value\" \"$plist\"\n}\ngetPlistWithKey() {\n local key=$1 plist=${2:-\"$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH\"}\n \n PlistBuddy -c \"Print :$key\" \"$plist\"\n}\nsetSettingWithTitle() {\n local i title=$1 value=$2 plist=${3:-\"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Settings.bundle/Root.plist\"}\n \n for (( i=0; 1; ++i )); do\n PlistBuddy -c \"Print :PreferenceSpecifiers:$i\" \"$plist\" &>/dev/null || break\n echo \"Checking preference specifier $i\"\n \n [[ $(PlistBuddy -c \"Print :PreferenceSpecifiers:$i:Title\" \"$plist\" 2>/dev/null) = $title ]] || continue\n\n echo \"Correct title, setting value.\"\n PlistBuddy -c \"Set :PreferenceSpecifiers:$i:DefaultValue $value\" \"$plist\"\n break\n done\n}\n\nbuild=$(git describe --tags --always --dirty --long)\ntag=$(git describe --tags | sed 's/-\\([^-]*\\)-[^-]*$/.\\1/')\n\nsetPlistWithKey CFBundleVersion \"$build\"\nsetPlistWithKey CFBundleShortVersionString \"$tag\"\n\nsetSettingWithTitle \"Build\" \"$build\"\nsetSettingWithTitle \"Version\" \"$tag\"\nsetSettingWithTitle \"Copyright\" \"$(getPlistWithKey NSHumanReadableCopyright)\"\n"; + shellScript = "Crashlytics/Crashlytics.framework/run \"$(/usr/libexec/PlistBuddy -c \"Print :'API Key'\" Crashlytics/Crashlytics.plist)\""; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -3787,19 +3876,12 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DABB9804150FF40100B05417 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DABB980F150FF40100B05417 /* SendToMac.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DAC632591486805C0075AEA5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DAC63280148680B10075AEA5 /* UIColor-Expanded.m in Sources */, + DAD312BC1552977200A3F9ED /* UIColor+Expanded.m in Sources */, + DAD312BE1552977200A3F9ED /* UIColor+HSV.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3851,6 +3933,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DAD3126D15528CD200A3F9ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DAD3128815528D0F00A3F9ED /* LocalyticsDatabase.m in Sources */, + DAD3128A15528D0F00A3F9ED /* LocalyticsSession.m in Sources */, + DAD3128C15528D0F00A3F9ED /* LocalyticsUploader.m in Sources */, + DAD3128E15528D0F00A3F9ED /* UploaderThread.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -3891,131 +3984,226 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_WARN_CXX0X_EXTENSIONS = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREPROCESSOR_DEFINITIONS = ( - "TESTFLIGHT=1", "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES; + GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES; + GCC_WARN_CHECK_SWITCH_STATEMENTS = YES; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; + GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MISSING_PARENTHESES = YES; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = NO; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_VALUE = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 5.0; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "${TARGET_NAME}"; SDKROOT = iphoneos; - STRIP_INSTALLED_PRODUCT = NO; + SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - DA5BFA6C147E415C00F98B1E /* Release */ = { + DA5BFA6C147E415C00F98B1E /* AdHoc */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_WARN_CXX0X_EXTENSIONS = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - COPY_PHASE_STRIP = NO; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREPROCESSOR_DEFINITIONS = ( - "TESTFLIGHT=1", + "ADHOC=1", "$(inherited)", + "NDEBUG=1", + "NS_BLOCK_ASSERTIONS=1", ); + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES; + GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES; + GCC_WARN_CHECK_SWITCH_STATEMENTS = YES; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; + GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MISSING_PARENTHESES = YES; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_VALUE = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 5.0; - OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "${TARGET_NAME}"; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; SDKROOT = iphoneos; - STRIP_INSTALLED_PRODUCT = NO; + SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; - name = Release; + name = AdHoc; }; DA5BFA6E147E415C00F98B1E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_ENTITLEMENTS = MasterPassword/iOS/MasterPassword.entitlements; - GCC_PRECOMPILE_PREFIX_HEADER = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/Crashlytics\"", + ); GCC_PREFIX_HEADER = "MasterPassword/iOS/MasterPassword-Prefix.pch"; INFOPLIST_FILE = "MasterPassword/iOS/MasterPassword-Info.plist"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/External/TestFlightSDK\"", + "\"$(SRCROOT)/TestFlight\"", ); OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - WRAPPER_EXTENSION = app; }; name = Debug; }; - DA5BFA6F147E415C00F98B1E /* Release */ = { + DA5BFA6F147E415C00F98B1E /* AdHoc */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_ENTITLEMENTS = MasterPassword/iOS/MasterPassword.entitlements; - GCC_PRECOMPILE_PREFIX_HEADER = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/Crashlytics\"", + ); GCC_PREFIX_HEADER = "MasterPassword/iOS/MasterPassword-Prefix.pch"; INFOPLIST_FILE = "MasterPassword/iOS/MasterPassword-Info.plist"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/External/TestFlightSDK\"", + "\"$(SRCROOT)/TestFlight\"", ); OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - WRAPPER_EXTENSION = app; }; - name = Release; + name = AdHoc; }; DA95D5A514DF063C008D1B94 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { DSTROOT = /tmp/InAppSettingsKit.dst; + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = InAppSettingsKit; SKIP_INSTALL = YES; }; name = Debug; }; - DA95D5A614DF063C008D1B94 /* Release */ = { + DA95D5A614DF063C008D1B94 /* AdHoc */ = { isa = XCBuildConfiguration; buildSettings = { DSTROOT = /tmp/InAppSettingsKit.dst; + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = InAppSettingsKit; SKIP_INSTALL = YES; }; - name = Release; + name = AdHoc; }; DA95D60914DF3F3B008D1B94 /* AppStore */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_WARN_CXX0X_EXTENSIONS = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - COPY_PHASE_STRIP = NO; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "APPSTORE=1", "$(inherited)", + "NDEBUG=1", + "NS_BLOCK_ASSERTIONS=1", ); + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES; + GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES; + GCC_WARN_CHECK_SWITCH_STATEMENTS = YES; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; + GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MISSING_PARENTHESES = YES; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_VALUE = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 5.0; - OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "${TARGET_NAME}"; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; SDKROOT = iphoneos; - STRIP_INSTALLED_PRODUCT = NO; + SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -4026,16 +4214,17 @@ buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_ENTITLEMENTS = MasterPassword/iOS/MasterPassword.entitlements; - GCC_PRECOMPILE_PREFIX_HEADER = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/Crashlytics\"", + ); GCC_PREFIX_HEADER = "MasterPassword/iOS/MasterPassword-Prefix.pch"; INFOPLIST_FILE = "MasterPassword/iOS/MasterPassword-Info.plist"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/External/TestFlightSDK\"", + "\"$(SRCROOT)/TestFlight\"", ); OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - WRAPPER_EXTENSION = app; }; name = AppStore; }; @@ -4081,60 +4270,13 @@ isa = XCBuildConfiguration; buildSettings = { DSTROOT = /tmp/InAppSettingsKit.dst; + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = InAppSettingsKit; SKIP_INSTALL = YES; }; name = AppStore; }; - DABB9811150FF40100B05417 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_ARC = YES; - DSTROOT = /tmp/SendToMac.dst; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "SendToMac/SendToMac-Prefix.pch"; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_UNINITIALIZED_AUTOS = YES; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = Debug; - }; - DABB9812150FF40100B05417 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_ARC = YES; - COPY_PHASE_STRIP = YES; - DSTROOT = /tmp/SendToMac.dst; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "SendToMac/SendToMac-Prefix.pch"; - GCC_WARN_UNINITIALIZED_AUTOS = YES; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = Release; - }; - DABB9813150FF40100B05417 /* AppStore */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_ARC = YES; - COPY_PHASE_STRIP = YES; - DSTROOT = /tmp/SendToMac.dst; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "SendToMac/SendToMac-Prefix.pch"; - GCC_WARN_UNINITIALIZED_AUTOS = YES; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = AppStore; - }; DAC632661486805C0075AEA5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4147,7 +4289,7 @@ }; name = Debug; }; - DAC632671486805C0075AEA5 /* Release */ = { + DAC632671486805C0075AEA5 /* AdHoc */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = NO; @@ -4157,7 +4299,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; - name = Release; + name = AdHoc; }; DAC63275148680650075AEA5 /* Debug */ = { isa = XCBuildConfiguration; @@ -4171,7 +4313,7 @@ }; name = Debug; }; - DAC63276148680650075AEA5 /* Release */ = { + DAC63276148680650075AEA5 /* AdHoc */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = NO; @@ -4181,7 +4323,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; - name = Release; + name = AdHoc; }; DAC77CB5148291A600BCF976 /* Debug */ = { isa = XCBuildConfiguration; @@ -4197,7 +4339,7 @@ }; name = Debug; }; - DAC77CB6148291A600BCF976 /* Release */ = { + DAC77CB6148291A600BCF976 /* AdHoc */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = NO; @@ -4209,7 +4351,28 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; - name = Release; + name = AdHoc; + }; + DAD3127A15528CD200A3F9ED /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + }; + name = Debug; + }; + DAD3127B15528CD200A3F9ED /* AdHoc */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + }; + name = AdHoc; + }; + DAD3127C15528CD200A3F9ED /* AppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + }; + name = AppStore; }; /* End XCBuildConfiguration section */ @@ -4218,70 +4381,71 @@ isa = XCConfigurationList; buildConfigurations = ( DA5BFA6B147E415C00F98B1E /* Debug */, - DA5BFA6C147E415C00F98B1E /* Release */, + DA5BFA6C147E415C00F98B1E /* AdHoc */, DA95D60914DF3F3B008D1B94 /* AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = AdHoc; }; DA5BFA6D147E415C00F98B1E /* Build configuration list for PBXNativeTarget "MasterPassword" */ = { isa = XCConfigurationList; buildConfigurations = ( DA5BFA6E147E415C00F98B1E /* Debug */, - DA5BFA6F147E415C00F98B1E /* Release */, + DA5BFA6F147E415C00F98B1E /* AdHoc */, DA95D60A14DF3F3B008D1B94 /* AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = AdHoc; }; DA95D5A414DF063C008D1B94 /* Build configuration list for PBXNativeTarget "InAppSettingsKit" */ = { isa = XCConfigurationList; buildConfigurations = ( DA95D5A514DF063C008D1B94 /* Debug */, - DA95D5A614DF063C008D1B94 /* Release */, + DA95D5A614DF063C008D1B94 /* AdHoc */, DA95D60E14DF3F3B008D1B94 /* AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DABB9810150FF40100B05417 /* Build configuration list for PBXNativeTarget "SendToMac" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DABB9811150FF40100B05417 /* Debug */, - DABB9812150FF40100B05417 /* Release */, - DABB9813150FF40100B05417 /* AppStore */, - ); - defaultConfigurationIsVisible = 0; + defaultConfigurationName = AdHoc; }; DAC632651486805C0075AEA5 /* Build configuration list for PBXNativeTarget "uicolor-utilities" */ = { isa = XCConfigurationList; buildConfigurations = ( DAC632661486805C0075AEA5 /* Debug */, - DAC632671486805C0075AEA5 /* Release */, + DAC632671486805C0075AEA5 /* AdHoc */, DA95D60C14DF3F3B008D1B94 /* AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = AdHoc; }; DAC63274148680650075AEA5 /* Build configuration list for PBXNativeTarget "jrswizzle" */ = { isa = XCConfigurationList; buildConfigurations = ( DAC63275148680650075AEA5 /* Debug */, - DAC63276148680650075AEA5 /* Release */, + DAC63276148680650075AEA5 /* AdHoc */, DA95D60D14DF3F3B008D1B94 /* AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = AdHoc; }; DAC77CB7148291A600BCF976 /* Build configuration list for PBXNativeTarget "Pearl" */ = { isa = XCConfigurationList; buildConfigurations = ( DAC77CB5148291A600BCF976 /* Debug */, - DAC77CB6148291A600BCF976 /* Release */, + DAC77CB6148291A600BCF976 /* AdHoc */, DA95D60B14DF3F3B008D1B94 /* AppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = AdHoc; + }; + DAD3127915528CD200A3F9ED /* Build configuration list for PBXNativeTarget "Localytics" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DAD3127A15528CD200A3F9ED /* Debug */, + DAD3127B15528CD200A3F9ED /* AdHoc */, + DAD3127C15528CD200A3F9ED /* AppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = AdHoc; }; /* End XCConfigurationList section */ diff --git a/MasterPassword-iOS.xcodeproj/xcshareddata/xcschemes/MasterPassword (Development).xcscheme b/MasterPassword-iOS.xcodeproj/xcshareddata/xcschemes/MasterPassword (Development).xcscheme index eda35017..d0732f12 100644 --- a/MasterPassword-iOS.xcodeproj/xcshareddata/xcschemes/MasterPassword (Development).xcscheme +++ b/MasterPassword-iOS.xcodeproj/xcshareddata/xcschemes/MasterPassword (Development).xcscheme @@ -68,14 +68,14 @@ shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" - buildConfiguration = "Release" + buildConfiguration = "AdHoc" debugDocumentVersioning = "YES"> diff --git a/MasterPassword/MPAppDelegate_Key.m b/MasterPassword/MPAppDelegate_Key.m index 8656e180..e791a408 100644 --- a/MasterPassword/MPAppDelegate_Key.m +++ b/MasterPassword/MPAppDelegate_Key.m @@ -42,9 +42,7 @@ static NSDictionary *keyHashQuery() { [PearlKeyChain deleteItemForQuery:keyHashQuery()]; [[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten]; -#endif } - (void)signOut { @@ -63,9 +61,7 @@ static NSDictionary *keyHashQuery() { // Key should not be stored in keychain. Delete it. dbg(@"Deleting key from key chain."); [PearlKeyChain deleteItemForQuery:keyQuery()]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored]; -#endif } } @@ -96,15 +92,11 @@ static NSDictionary *keyHashQuery() { if (![keyHash isEqual:tryKeyHash]) { dbg(@"Key phrase hash mismatch. Expected: %@, answer: %@.", keyHash, tryKeyHash); -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch]; -#endif return NO; } -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointMPAsked]; -#endif [self updateKey:tryKey]; return YES; @@ -142,9 +134,7 @@ static NSDictionary *keyHashQuery() { nil]]; } -#ifdef TESTFLIGHT [TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSetKeyphraseLength, key.length]]; -#endif } } diff --git a/MasterPassword/MPElementGeneratedEntity.h b/MasterPassword/MPElementGeneratedEntity.h index 8103a226..832ca61b 100644 --- a/MasterPassword/MPElementGeneratedEntity.h +++ b/MasterPassword/MPElementGeneratedEntity.h @@ -13,6 +13,6 @@ @interface MPElementGeneratedEntity : MPElementEntity -@property (nonatomic, assign) uint16_t counter; +@property (nonatomic, assign) int16_t counter; @end diff --git a/MasterPassword/MPElementGeneratedEntity.m b/MasterPassword/MPElementGeneratedEntity.m index b2827d42..04e549c9 100644 --- a/MasterPassword/MPElementGeneratedEntity.m +++ b/MasterPassword/MPElementGeneratedEntity.m @@ -22,7 +22,7 @@ return nil; if (self.type & MPElementTypeClassCalculated) - return MPCalculateContent(self.type, self.name, [MPAppDelegate get].key, self.counter); + return MPCalculateContent((unsigned)self.type, self.name, [MPAppDelegate get].key, self.counter); @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"Unsupported type: %d", self.type] userInfo:nil]; diff --git a/MasterPassword/MPTypes.h b/MasterPassword/MPTypes.h index c73b94ef..d13a12f8 100644 --- a/MasterPassword/MPTypes.h +++ b/MasterPassword/MPTypes.h @@ -32,10 +32,10 @@ typedef enum { MPElementTypeStoredDevicePrivate = MPElementTypeClassStored | 0x02, } MPElementType; -#ifdef TESTFLIGHT #define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction" #define MPTestFlightCheckpointHelpChapter @"MPTestFlightCheckpointHelpChapter_%@" #define MPTestFlightCheckpointCopyToPasteboard @"MPTestFlightCheckpointCopyToPasteboard" +#define MPTestFlightCheckpointResetPasswordCounter @"MPTestFlightCheckpointResetPasswordCounter" #define MPTestFlightCheckpointIncrementPasswordCounter @"MPTestFlightCheckpointIncrementPasswordCounter" #define MPTestFlightCheckpointEditPassword @"MPTestFlightCheckpointEditPassword" #define MPTestFlightCheckpointCloseAlert @"MPTestFlightCheckpointCloseAlert" @@ -56,7 +56,6 @@ typedef enum { #define MPTestFlightCheckpointMPAsked @"MPTestFlightCheckpointMPAsked" #define MPTestFlightCheckpointStoreIncompatible @"MPTestFlightCheckpointStoreIncompatible" #define MPTestFlightCheckpointSetKeyphraseLength @"MPTestFlightCheckpointSetKeyphraseLength_%d" -#endif #define MPNotificationStoreUpdated @"MPNotificationStoreUpdated" #define MPNotificationKeySet @"MPNotificationKeySet" @@ -69,4 +68,4 @@ NSData *keyHashForKey(NSData *key); NSString *NSStringFromMPElementType(MPElementType type); NSString *ClassNameFromMPElementType(MPElementType type); Class ClassFromMPElementType(MPElementType type); -NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint16_t counter); +NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int16_t counter); diff --git a/MasterPassword/MPTypes.m b/MasterPassword/MPTypes.m index 9934ead5..4589a7cf 100644 --- a/MasterPassword/MPTypes.m +++ b/MasterPassword/MPTypes.m @@ -102,7 +102,7 @@ NSString *ClassNameFromMPElementType(MPElementType type) { } static NSDictionary *MPTypes_ciphers = nil; -NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint16_t counter) { +NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int16_t counter) { assert(type & MPElementTypeClassCalculated); diff --git a/MasterPassword/iOS/MPAppDelegate.m b/MasterPassword/iOS/MPAppDelegate.m index 7b626301..505f615b 100644 --- a/MasterPassword/iOS/MPAppDelegate.m +++ b/MasterPassword/iOS/MPAppDelegate.m @@ -11,6 +11,23 @@ #import "MPMainViewController.h" #import "IASKSettingsReader.h" +#import "LocalyticsSession.h" +#import "TestFlight.h" +#import + +@interface MPAppDelegate () + +- (NSString *)testFlightInfo; +- (NSString *)testFlightToken; + +- (NSString *)crashlyticsInfo; +- (NSString *)crashlyticsAPIKey; + +- (NSString *)localyticsInfo; +- (NSString *)localyticsKey; + +@end + @implementation MPAppDelegate @@ -28,30 +45,70 @@ #ifdef DEBUG [PearlLogger get].autoprintLevel = PearlLogLevelDebug; - [NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)]; +// [NSClassFromString(@"WebView") performSelector:NSSelectorFromString(@"_enableRemoteInspector")]; #endif } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { -#ifdef TESTFLIGHT - @try { - [TestFlight takeOff:@"bd44885deee7adce0645ce8e5498d80a_NDQ5NDQyMDExLTEyLTAyIDExOjM1OjQ4LjQ2NjM4NA"]; - [TestFlight setOptions:[NSDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithBool:NO], @"logToConsole", - [NSNumber numberWithBool:NO], @"logToSTDERR", - nil]]; - [TestFlight passCheckpoint:MPTestFlightCheckpointLaunched]; - [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { - if (message.level >= PearlLogLevelInfo) - TFLog(@"%@", message); - - return YES; - }]; - } - @catch (NSException *exception) { - err(@"TestFlight: %@", exception); - } +#ifndef DEBUG + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + @try { + NSString *testFlightToken = [self testFlightToken]; + if ([testFlightToken length]) { + dbg(@"Initializing TestFlight"); + [TestFlight addCustomEnvironmentInformation:@"Anonymous" forKey:@"username"]; + [TestFlight setOptions:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:NO], @"logToConsole", + [NSNumber numberWithBool:NO], @"logToSTDERR", + nil]]; + [TestFlight takeOff:testFlightToken]; + [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { + if (message.level >= PearlLogLevelInfo) + TFLog(@"%@", message); + + return YES; + }]; + [TestFlight passCheckpoint:MPTestFlightCheckpointLaunched]; + } + } + @catch (NSException *exception) { + err(@"TestFlight: %@", exception); + } + @try { + NSString *crashlyticsAPIKey = [self crashlyticsAPIKey]; + if ([crashlyticsAPIKey length]) { + dbg(@"Initializing Crashlytics"); + //[Crashlytics sharedInstance].debugMode = YES; + [Crashlytics startWithAPIKey:crashlyticsAPIKey afterDelay:0]; + } + } + @catch (NSException *exception) { + err(@"Crashlytics: %@", exception); + } + @try { + NSString *localyticsKey = [self localyticsKey]; + if ([localyticsKey length]) { + dbg(@"Initializing Localytics"); + [[LocalyticsSession sharedLocalyticsSession] startSession:localyticsKey]; + [[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) { + if (message.level >= PearlLogLevelError) + [[LocalyticsSession sharedLocalyticsSession] tagEvent:@"Problem" attributes: + [NSDictionary dictionaryWithObjectsAndKeys: + [message levelDescription], + @"level", + message.message, + @"message", + nil]]; + + return YES; + }]; + } + } + @catch (NSException *exception) { + err(@"Localytics exception: %@", exception); + } + }); #endif UIImage *navBarImage = [[UIImage imageNamed:@"ui_navbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)]; @@ -59,10 +116,10 @@ [[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsLandscapePhone]; [[UINavigationBar appearance] setTitleTextAttributes: [NSDictionary dictionaryWithObjectsAndKeys: - [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:1.0], UITextAttributeTextColor, - [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8], UITextAttributeTextShadowColor, - [NSValue valueWithUIOffset:UIOffsetMake(0, -1)], UITextAttributeTextShadowOffset, - [UIFont fontWithName:@"Helvetica-Neue" size:0.0], UITextAttributeFont, + [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f], UITextAttributeTextColor, + [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.8f], UITextAttributeTextShadowColor, + [NSValue valueWithUIOffset:UIOffsetMake(0, -1)], UITextAttributeTextShadowOffset, + [UIFont fontWithName:@"Helvetica-Neue" size:0.0f], UITextAttributeFont, nil]]; UIImage *navBarButton = [[UIImage imageNamed:@"ui_navbar_button"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)]; @@ -73,10 +130,10 @@ [[UIBarButtonItem appearance] setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone]; [[UIBarButtonItem appearance] setTitleTextAttributes: [NSDictionary dictionaryWithObjectsAndKeys: - [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0], UITextAttributeTextColor, - [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5], UITextAttributeTextShadowColor, - [NSValue valueWithUIOffset:UIOffsetMake(0, 1)], UITextAttributeTextShadowOffset, - [UIFont fontWithName:@"Helvetica-Neue" size:0.0], UITextAttributeFont, + [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f], UITextAttributeTextColor, + [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5f], UITextAttributeTextShadowColor, + [NSValue valueWithUIOffset:UIOffsetMake(0, 1)], UITextAttributeTextShadowOffset, + [UIFont fontWithName:@"Helvetica-Neue" size:0.0f], UITextAttributeFont, nil] forState:UIControlStateNormal]; @@ -118,7 +175,7 @@ [self loadKey:YES]; }]; -#ifdef TESTFLIGHT +#ifdef ADHOC [PearlAlert showAlertWithTitle:@"Welcome, tester!" message: @"Thank you for taking the time to test Master Password.\n\n" @"Please provide any feedback, however minor it may seem, via the Feedback action item accessible from the top right.\n\n" @@ -143,18 +200,14 @@ else [self loadKey:NO]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointActivated]; -#endif } - (void)showGuide { [self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide]; -#endif } - (void)loadKey:(BOOL)animated { @@ -172,6 +225,34 @@ }); } +- (void)applicationDidEnterBackground:(UIApplication *)application { + + [[LocalyticsSession sharedLocalyticsSession] close]; + [[LocalyticsSession sharedLocalyticsSession] upload]; + + [super applicationDidEnterBackground:application]; +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + + [[LocalyticsSession sharedLocalyticsSession] resume]; + [[LocalyticsSession sharedLocalyticsSession] upload]; + + [super applicationWillEnterForeground:application]; +} + +- (void)applicationWillTerminate:(UIApplication *)application { + + [self saveContext]; + + [TestFlight passCheckpoint:MPTestFlightCheckpointTerminated]; + + [[LocalyticsSession sharedLocalyticsSession] close]; + [[LocalyticsSession sharedLocalyticsSession] upload]; + + [super applicationWillTerminate:application]; +} + - (void)applicationWillResignActive:(UIApplication *)application { [self saveContext]; @@ -179,18 +260,7 @@ if (![[MPiOSConfig get].rememberKey boolValue]) [self updateKey:nil]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointDeactivated]; -#endif -} - -- (void)applicationWillTerminate:(UIApplication *)application { - - [self saveContext]; - -#ifdef TESTFLIGHT - [TestFlight passCheckpoint:MPTestFlightCheckpointTerminated]; -#endif } + (NSManagedObjectContext *)managedObjectContext { @@ -279,9 +349,7 @@ [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]; #endif -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointStoreIncompatible]; -#endif @throw [NSException exceptionWithName:error.domain reason:error.localizedDescription userInfo:[NSDictionary dictionaryWithObject:error forKey:@"cause"]]; @@ -306,4 +374,70 @@ return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; } + +#pragma mark - TestFlight + + +static NSDictionary *testFlightInfo = nil; + +- (NSDictionary *)testFlightInfo { + + if (testFlightInfo == nil) + testFlightInfo = [[NSDictionary alloc] initWithContentsOfURL: + [[NSBundle mainBundle] URLForResource:@"TestFlight" withExtension:@"plist"]]; + + return testFlightInfo; +} + +- (NSString *)testFlightToken { + + return NSNullToNil([[self testFlightInfo] valueForKeyPath:@"Team Token"]); +} + + +#pragma mark - Crashlytics + + +static NSDictionary *crashlyticsInfo = nil; + +- (NSDictionary *)crashlyticsInfo { + + if (crashlyticsInfo == nil) + crashlyticsInfo = [[NSDictionary alloc] initWithContentsOfURL: + [[NSBundle mainBundle] URLForResource:@"Crashlytics" withExtension:@"plist"]]; + + return crashlyticsInfo; +} + +- (NSString *)crashlyticsAPIKey { + + return NSNullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]); +} + + +#pragma mark - Localytics + + +static NSDictionary *localyticsInfo = nil; + +- (NSDictionary *)localyticsInfo { + + if (localyticsInfo == nil) + localyticsInfo = [[NSDictionary alloc] initWithContentsOfURL: + [[NSBundle mainBundle] URLForResource:@"Localytics" withExtension:@"plist"]]; + + return localyticsInfo; +} + +- (NSString *)localyticsKey { + +#ifdef DEBUG + return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]); +#elif defined(LITE) + return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution.lite"]); +#else + return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]); +#endif +} + @end diff --git a/MasterPassword/iOS/MPMainViewController.h b/MasterPassword/iOS/MPMainViewController.h index 145dbe4a..038a2d26 100644 --- a/MasterPassword/iOS/MPMainViewController.h +++ b/MasterPassword/iOS/MPMainViewController.h @@ -34,6 +34,7 @@ - (IBAction)copyContent; - (IBAction)incrementPasswordCounter; +- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender; - (IBAction)editPassword; - (IBAction)closeAlert; - (IBAction)action:(UIBarButtonItem *)sender; diff --git a/MasterPassword/iOS/MPMainViewController.m b/MasterPassword/iOS/MPMainViewController.m index 93576aa5..5e341383 100644 --- a/MasterPassword/iOS/MPMainViewController.m +++ b/MasterPassword/iOS/MPMainViewController.m @@ -21,7 +21,8 @@ - (void)updateWasAnimated:(BOOL)animated; - (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon; - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message; -- (void)updateElement:(void (^)(void))updateElement; +- (void)changeElementWithWarning:(NSString *)warning do:(void (^)(void))task; +- (void)changeElementWithoutWarningDo:(void (^)(void))task; @end @@ -163,9 +164,9 @@ self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassCalculated? 0.5f: 0; self.passwordEdit.alpha = self.activeElement.type & MPElementTypeClassStored? 0.5f: 0; - [self.typeButton setTitle:NSStringFromMPElementType(self.activeElement.type) + [self.typeButton setTitle:NSStringFromMPElementType((unsigned)self.activeElement.type) forState:UIControlStateNormal]; - self.typeButton.alpha = NSStringFromMPElementType(self.activeElement.type).length? 1: 0; + self.typeButton.alpha = NSStringFromMPElementType((unsigned)self.activeElement.type).length? 1: 0; self.contentField.enabled = NO; @@ -212,9 +213,7 @@ - (void)setHelpChapter:(NSString *)chapter { -#ifdef TESTFLIGHT [TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointHelpChapter, chapter]]; -#endif dispatch_async(dispatch_get_main_queue(), ^{ [self.helpView loadRequest: @@ -227,7 +226,7 @@ - (void)webViewDidFinishLoad:(UIWebView *)webView { NSString *error = [self.helpView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"setClass('%@');", - ClassNameFromMPElementType(self.activeElement.type)]]; + ClassNameFromMPElementType((unsigned)self.activeElement.type)]]; if (error.length) err(@"helpView.setClass: %@", error); } @@ -285,42 +284,76 @@ [self showContentTip:@"Copied!" withIcon:nil]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointCopyToPasteboard]; -#endif } - (IBAction)incrementPasswordCounter { if (![self.activeElement isKindOfClass:[MPElementGeneratedEntity class]]) - // Not of a type that supports a password counter; + // Not of a type that supports a password counter. return; - [self updateElement:^{ - ++((MPElementGeneratedEntity *) self.activeElement).counter; - }]; + [self changeElementWithWarning: + @"You are incrementing the site's password counter.\n\n" + @"If you continue, a new password will be generated for this site. " + @"You will then need to update your account's old password to this newly generated password.\n" + @"You can reset the counter by holding down on this button." + do:^{ + ++((MPElementGeneratedEntity *) self.activeElement).counter; + }]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointIncrementPasswordCounter]; -#endif } -- (void)updateElement:(void (^)(void))updateElement { +- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender { - // Update password counter. + if (![self.activeElement isKindOfClass:[MPElementGeneratedEntity class]]) + // Not of a type that supports a password counter. + return; + if (((MPElementGeneratedEntity *)self.activeElement).counter == 1) + // Counter has initial value, no point resetting. + return; + + [self changeElementWithWarning: + @"You are resetting the site's password counter.\n\n" + @"If you continue, the site's password will change back to its original value. " + @"You will then need to update your account's password back to this original value." + do:^{ + ((MPElementGeneratedEntity *) self.activeElement).counter = 1; + }]; + + [TestFlight passCheckpoint:MPTestFlightCheckpointResetPasswordCounter]; +} + +- (void)changeElementWithWarning:(NSString *)warning do:(void (^)(void))task; { + + [PearlAlert showAlertWithTitle:@"Password Change" message:warning viewStyle:UIAlertViewStyleDefault + tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { + if (buttonIndex == [alert cancelButtonIndex]) + return; + + [self changeElementWithoutWarningDo:task]; + } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonContinue, nil]; +} + +- (void)changeElementWithoutWarningDo:(void (^)(void))task; { + + // Update element, keeping track of the old password. NSString *oldPassword = self.activeElement.description; - updateElement(); + task(); NSString *newPassword = self.activeElement.description; [self updateAnimated:YES]; // Show new and old password. if ([oldPassword length] && ![oldPassword isEqualToString:newPassword]) [self showAlertWithTitle:@"Password Changed!" message:l(@"The password for %@ has changed.\n\n" + @"IMPORTANT:\n" @"Don't forget to update the site with your new password! " @"Your old password was:\n" @"%@", self.activeElement.name, oldPassword)]; } + - (IBAction)editPassword { if (self.activeElement.type & MPElementTypeClassStored) { @@ -328,9 +361,7 @@ [self.contentField becomeFirstResponder]; } -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointEditPassword]; -#endif } - (IBAction)closeAlert { @@ -342,9 +373,7 @@ self.alertBody.text = nil; }]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointCloseAlert]; -#endif } - (IBAction)action:(id)sender { @@ -371,7 +400,7 @@ [self.navigationController pushViewController:settingsVC animated:YES]; break; } -#ifdef TESTFLIGHT +#ifdef ADHOC case 4: [TestFlight openFeedbackView]; break; @@ -386,13 +415,11 @@ } } -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointAction]; -#endif } cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil otherTitles: [self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", -#ifdef TESTFLIGHT +#ifdef ADHOC @"Feedback", #endif @"Sign Out", @@ -401,36 +428,38 @@ - (MPElementType)selectedType { - return self.activeElement.type; + return (unsigned)self.activeElement.type; } - (void)didSelectType:(MPElementType)type { - [self updateElement:^{ - // Update password type. - if (ClassFromMPElementType(type) != ClassFromMPElementType(self.activeElement.type)) - // Type requires a different class of element. Recreate the element. - [[MPAppDelegate managedObjectContext] performBlockAndWait:^{ - MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type) - inManagedObjectContext:[MPAppDelegate managedObjectContext]]; - newElement.name = self.activeElement.name; - newElement.mpHashHex = self.activeElement.mpHashHex; - newElement.uses = self.activeElement.uses; - newElement.lastUsed = self.activeElement.lastUsed; - - [[MPAppDelegate managedObjectContext] deleteObject:self.activeElement]; - self.activeElement = newElement; - }]; - - self.activeElement.type = type; - -#ifdef TESTFLIGHT - [TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSelectType, NSStringFromMPElementType(type)]]; -#endif - - if (type & MPElementTypeClassStored && ![self.activeElement.description length]) - [self showContentTip:@"Tap to set a password." withIcon:self.contentTipEditIcon]; - }]; + [self changeElementWithWarning: + @"You are about to change the type of this password.\n\n" + @"If you continue, the password for this site will change. " + @"You will need to update your account's old password to the new one." + do:^{ + // Update password type. + if (ClassFromMPElementType(type) != ClassFromMPElementType((unsigned)self.activeElement.type)) + // Type requires a different class of element. Recreate the element. + [[MPAppDelegate managedObjectContext] performBlockAndWait:^{ + MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type) + inManagedObjectContext:[MPAppDelegate managedObjectContext]]; + newElement.name = self.activeElement.name; + newElement.mpHashHex = self.activeElement.mpHashHex; + newElement.uses = self.activeElement.uses; + newElement.lastUsed = self.activeElement.lastUsed; + + [[MPAppDelegate managedObjectContext] deleteObject:self.activeElement]; + self.activeElement = newElement; + }]; + + self.activeElement.type = type; + + [TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSelectType, NSStringFromMPElementType(type)]]; + + if (type & MPElementTypeClassStored && ![self.activeElement.description length]) + [self showContentTip:@"Tap to set a password." withIcon:self.contentTipEditIcon]; + }]; } - (void)didSelectElement:(MPElementEntity *)element { @@ -443,17 +472,14 @@ [self showAlertWithTitle:@"New Site" message: l(@"You've just created a password for %@.\n\n" @"IMPORTANT:\n" - @"Don't forget to set or change the password for your account at %@ to the password above. " - @"It's best to do this right away. If you forget it, may get confusing later on " - @"to remember what password you need to use for logging into the site.", + @"Go to %@ and set or change the password for your account to the password above.\n" + @"Do this right away: if you forget, you may have trouble remembering which password to use to log into the site later on.", self.activeElement.name, self.activeElement.name)]; [self.searchDisplayController setActive:NO animated:YES]; self.searchDisplayController.searchBar.text = self.activeElement.name; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointSelectElement]; -#endif } [self updateAnimated:YES]; @@ -479,7 +505,7 @@ // Content hasn't changed. return; - [self updateElement:^{ + [self changeElementWithoutWarningDo:^{ ((MPElementStoredEntity *) self.activeElement).content = self.contentField.text; }]; } @@ -489,9 +515,7 @@ navigationType:(UIWebViewNavigationType)navigationType { if (navigationType == UIWebViewNavigationTypeLinkClicked) { -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointExternalLink]; -#endif [[UIApplication sharedApplication] openURL:[request URL]]; return NO; diff --git a/MasterPassword/iOS/MPSearchDelegate.m b/MasterPassword/iOS/MPSearchDelegate.m index 2083530e..92c2f29a 100644 --- a/MasterPassword/iOS/MPSearchDelegate.m +++ b/MasterPassword/iOS/MPSearchDelegate.m @@ -41,8 +41,8 @@ - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { UITableView *tableView = self.searchDisplayController.searchResultsTableView; - for (NSUInteger section = 0; section < [self numberOfSectionsInTableView:tableView]; ++section) { - NSUInteger rowCount = [self tableView:tableView numberOfRowsInSection:section]; + for (NSInteger section = 0; section < [self numberOfSectionsInTableView:tableView]; ++section) { + NSInteger rowCount = [self tableView:tableView numberOfRowsInSection:section]; if (!rowCount) continue; @@ -54,9 +54,7 @@ - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointCancelSearch]; -#endif [self.delegate didSelectElement:nil]; } @@ -189,13 +187,13 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return [[self.fetchedResultsController sections] count] + ([self.query length]? 1: 0); + return (signed)[[self.fetchedResultsController sections] count] + ([self.query length]? 1: 0); } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - if (section < [[self.fetchedResultsController sections] count]) - return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects]; + if (section < (signed)[[self.fetchedResultsController sections] count]) + return (signed)[[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] numberOfObjects]; return 1; } @@ -230,7 +228,7 @@ - (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section < [[self.fetchedResultsController sections] count]) { + if (indexPath.section < (signed)[[self.fetchedResultsController sections] count]) { MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath]; cell.textLabel.text = element.name; @@ -245,7 +243,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section < [[self.fetchedResultsController sections] count]) + if (indexPath.section < (signed)[[self.fetchedResultsController sections] count]) [self.delegate didSelectElement:[self.fetchedResultsController objectAtIndexPath:indexPath]]; else { @@ -263,7 +261,7 @@ [self.fetchedResultsController.managedObjectContext performBlock:^{ MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class]) inManagedObjectContext:self.fetchedResultsController.managedObjectContext]; - assert([element isKindOfClass:ClassFromMPElementType(element.type)]); + assert([element isKindOfClass:ClassFromMPElementType((unsigned)element.type)]); assert([MPAppDelegate get].keyHashHex); element.name = siteName; @@ -279,8 +277,8 @@ - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - if (section < [[self.fetchedResultsController sections] count]) - return [[[self.fetchedResultsController sections] objectAtIndex:section] name]; + if (section < (signed)[[self.fetchedResultsController sections] count]) + return [[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] name]; return @""; } @@ -297,15 +295,13 @@ - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section < [[self.fetchedResultsController sections] count]) { + if (indexPath.section < (signed)[[self.fetchedResultsController sections] count]) { if (editingStyle == UITableViewCellEditingStyleDelete) [self.fetchedResultsController.managedObjectContext performBlock:^{ MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath]; [self.fetchedResultsController.managedObjectContext deleteObject:element]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointDeleteElement]; -#endif }]; } } diff --git a/MasterPassword/iOS/MPUnlockViewController.m b/MasterPassword/iOS/MPUnlockViewController.m index 4f1c705e..37acc4b3 100644 --- a/MasterPassword/iOS/MPUnlockViewController.m +++ b/MasterPassword/iOS/MPUnlockViewController.m @@ -173,7 +173,7 @@ typedef enum { dispatch_async(dispatch_get_main_queue(), ^{ [self showMessage:@"Success!" state:MPLockscreenSuccess]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 1.5f), dispatch_get_main_queue(), ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (long)(NSEC_PER_SEC * 1.5f)), dispatch_get_main_queue(), ^{ [self dismissModalViewControllerAnimated:YES]; }); }); @@ -214,9 +214,7 @@ typedef enum { [[MPAppDelegate get] loadKey:YES]; -#ifdef TESTFLIGHT [TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged]; -#endif } cancelTitle:[PearlStrings get].commonButtonAbort otherTitles:[PearlStrings get].commonButtonContinue, nil]; diff --git a/MasterPassword/iOS/MainStoryboard_iPhone.storyboard b/MasterPassword/iOS/MainStoryboard_iPhone.storyboard index bfa12a90..e2c4c185 100644 --- a/MasterPassword/iOS/MainStoryboard_iPhone.storyboard +++ b/MasterPassword/iOS/MainStoryboard_iPhone.storyboard @@ -1,8 +1,9 @@ - + + - + @@ -399,6 +400,7 @@ The passwords aren't saved anywhere. This is a major advantage: if you lose you