1149 lines
47 KiB
Mathematica
1149 lines
47 KiB
Mathematica
|
// 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 <sys/types.h>
|
|||
|
#include <sys/sysctl.h>
|
|||
|
#include <mach/mach.h>
|
|||
|
#include <sys/socket.h>
|
|||
|
#include <net/if_dl.h>
|
|||
|
#include <ifaddrs.h>
|
|||
|
#include <CommonCrypto/CommonDigest.h>
|
|||
|
|
|||
|
#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; i<dlAddr->sdl_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
|