744 lines
27 KiB
Mathematica
744 lines
27 KiB
Mathematica
|
//
|
||
|
// 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
|