2012-09-12 13:58:54 +00:00
// LocalyticsSession . m
// Copyright ( C ) 2012 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
2012-09-21 17:42:18 +00:00
# define CLIENT_VERSION @ "iOS_2.14" // The version of this library
2012-09-12 13:58:54 +00:00
# define LOCALYTICS_DIR @ ".localytics" // The directory in which the Localytics database is stored
# define IFT_ETHER 0 x6 // 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 ;
@ property ( nonatomic , assign ) NSInteger sessionNumber ;
// Private methods .
- ( void ) ll_open ;
- ( void ) reopenPreviousSession ;
- ( void ) addFlowEventWithName : ( NSString * ) name type : ( NSString * ) eventType ;
- ( void ) addScreenWithName : ( NSString * ) name ;
- ( NSString * ) blobHeaderStringWithSequenceNumber : ( int ) nextSequenceNumber ;
- ( BOOL ) ll_isOptedIn ;
- ( BOOL ) createOptEvent : ( BOOL ) optState ;
- ( BOOL ) saveApplicationFlowAndRemoveOnResume : ( BOOL ) removeOnResume ;
- ( NSString * ) formatAttributeWithName : ( NSString * ) paramName value : ( NSString * ) paramValue ;
- ( NSString * ) formatAttributeWithName : ( NSString * ) paramName value : ( NSString * ) paramValue first : ( BOOL ) firstAttribute ;
- ( void ) uploadCallback : ( NSDictionary * ) info ;
// Datapoint methods .
- ( NSString * ) customDimensions ;
- ( NSString * ) locationDimensions ;
- ( NSString * ) hashString : ( NSString * ) input ;
- ( NSString * ) randomUUID ;
- ( NSString * ) escapeString : ( NSString * ) input ;
- ( NSString * ) installationId ;
- ( NSString * ) appVersion ;
- ( NSTimeInterval ) currentTimestamp ;
- ( BOOL ) isDeviceJailbroken ;
- ( NSString * ) deviceModel ;
- ( NSString * ) modelSizeString ;
- ( double ) availableMemory ;
- ( NSString * ) advertisingIdentifier ;
- ( NSString * ) uniqueDeviceIdentifier ;
@ end
@ implementation LocalyticsSession
@ synthesize queue = _queue ;
@ synthesize criticalGroup = _criticalGroup ;
@ synthesize sessionUUID = _sessionUUID ;
@ synthesize 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 ;
@ synthesize sessionNumber = _sessionNumber ;
@ synthesize enableHTTPS = _enableHTTPS ;
// Stores the last location passed in to the app .
CLLocationCoordinate2D lastDeviceLocation = { 0 } ;
# 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 ( ) ;
_enableHTTPS = NO ;
[ [ 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 {
// check app key
NSPredicate * matchPred = [ NSPredicate predicateWithFormat : @ "SELF MATCHES %@" , @ "[A-Fa-f0-9-]+" ] ;
BOOL matches = [ matchPred evaluateWithObject : appKey ] ;
if ( matches = = NO ) {
// generate exception
NSException * exception = [ NSException exceptionWithName : @ "Invalid Localytics App Key" reason : @ "Application key is not valid. Please look at the iOS integration guidlines at http://www.localytics.com/docs/iphone-integration/" userInfo : nil ] ;
[ exception raise ] ;
}
[ self LocalyticsSession : appKey ] ;
[ self open ] ;
[ self upload ] ;
}
// Public interface to ll_open .
- ( void ) open {
dispatch_async ( _queue , ^ {
[ self ll_open ] ;
} ) ;
}
- ( BOOL ) resume {
2012-09-21 17:42:18 +00:00
// Do nothing if session is already open
if ( self . isSessionOpen = = YES )
return YES ;
BOOL ret = NO ;
// conditions for resuming previous session
if ( self . sessionHasBeenOpen &&
( ! self . sessionCloseTime ||
[ self . sessionCloseTime timeIntervalSinceNow ] * -1 <= self . backgroundSessionTimeout ) ) {
// Note that we allow the session to be resumed even if the database size exceeds the
// maximum . This is because we don ' t want to create incomplete sessions . If the DB was large
// enough that the previous session could not be opened , there will be nothing to resume . But
// if this session caused it to go over it is better to let it complete and stop the next one
// from being created .
ret = YES ;
dispatch_async ( _queue , ^ {
@ try {
if ( [ self ll_isOptedIn ] = = false ) {
[ self logMessage : @ "Can't resume session because user is opted out." ] ;
} else {
[ self logMessage : @ "Resume called - Resuming previous session." ] ;
[ self reopenPreviousSession ] ;
}
}
@ catch ( NSException * e ) { }
} ) ;
} else {
ret = NO ;
dispatch_async ( _queue , ^ {
@ try {
if ( [ self ll_isOptedIn ] = = false ) {
[ self logMessage : @ "Can't resume session because user is opted out." ] ;
} else {
// otherwise open new session and upload
[ self logMessage : @ "Resume called - Opening a new session." ] ;
[ self ll_open ] ;
}
}
@ catch ( NSException * e ) { }
} ) ;
}
self . sessionCloseTime = nil ;
return ret ;
2012-09-12 13:58:54 +00:00
}
- ( void ) close {
dispatch_group _async ( _criticalGroup , _queue , ^ {
// Do nothing if the session is not open
if ( self . isSessionOpen = = NO ) {
[ self logMessage : @ "Unable to close session" ] ;
return ;
}
// Save time of close
self . sessionCloseTime = [ NSDate date ] ;
// Update active session duration .
self . sessionActiveDuration + = [ self . sessionCloseTime timeIntervalSinceDate : self . sessionResumeTime ] ;
int sessionLength = ( int ) [ [ NSDate date ] timeIntervalSince1970 ] - self . lastSessionStartTimestamp ;
@ try {
// Create the JSON representing the close blob
NSMutableString * closeEventString = [ NSMutableString string ] ;
[ closeEventString appendString : @ "{" ] ;
[ closeEventString appendString : [ self formatAttributeWithName : PARAM_DATA _TYPE value : @ "c" first : YES ] ] ;
[ closeEventString appendString : [ self formatAttributeWithName : PARAM_SESSION _UUID value : self . sessionUUID ] ] ;
[ closeEventString appendString : [ self formatAttributeWithName : PARAM_UUID value : [ self randomUUID ] ] ] ;
[ closeEventString appendFormat : @ ",\" % @ \ ":%ld" , PARAM_SESSION _START , ( long ) self . lastSessionStartTimestamp ] ;
[ closeEventString appendFormat : @ ",\" % @ \ ":%ld" , PARAM_SESSION _ACTIVE , ( long ) self . sessionActiveDuration ] ;
[ closeEventString appendFormat : @ ",\" % @ \ ":%ld" , PARAM_CLIENT _TIME , ( long ) [ self currentTimestamp ] ] ;
// Avoid recording session lengths of users with unreasonable client times ( usually caused by developers testing clock change attacks )
if ( sessionLength > 0 && sessionLength < 400000 ) {
[ closeEventString appendFormat : @ ",\" % @ \ ":%d" , PARAM_SESSION _TOTAL , sessionLength ] ;
}
// Open second level - screen flow
[ closeEventString appendFormat : @ ",\" % @ \ ":[" , PARAM_SESSION _SCREENFLOW ] ;
[ closeEventString appendString : self . screens ] ;
// Close second level - screen flow
[ closeEventString appendString : @ "]" ] ;
// Append the custom dimensions
[ closeEventString appendString : [ self customDimensions ] ] ;
// Append the location
[ closeEventString appendString : [ self locationDimensions ] ] ;
// Close first level - close blob
[ closeEventString appendString : @ "}\n" ] ;
BOOL success = [ [ LocalyticsDatabase sharedLocalyticsDatabase ] queueCloseEventWithBlobString : [ [ closeEventString copy ] autorelease ] ] ;
self . isSessionOpen = NO ; // Session is no longer open .
if ( success ) {
[ self logMessage : @ "Session succesfully closed." ] ;
} else {
[ self logMessage : @ "Failed to record session close." ] ;
}
}
@ catch ( NSException * e ) { }
} ) ;
}
- ( void ) 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 : @ ",\" % @ \ ":%ld" , PARAM_CLIENT _TIME , ( long ) [ self currentTimestamp ] ] ;
// Append the custom dimensions
[ eventString appendString : [ self customDimensions ] ] ;
// Append the location
[ eventString appendString : [ self locationDimensions ] ] ;
// If there are any attributes for this event , add them as a hash
int attrIndex = 0 ;
if ( attributes ! = nil )
{
// Open second level - attributes
[ eventString appendString : [ NSString stringWithFormat : @ ",\" % @ \ ":{" , PARAM_ATTRIBUTES ] ] ;
for ( id key in [ attributes allKeys ] )
{
// Have to escape paramName and paramValue because they user - defined .
[ eventString appendString :
[ self formatAttributeWithName : [ self escapeString : [ key description ] ]
value : [ self escapeString : [ [ attributes valueForKey : key ] description ] ]
first : ( attrIndex = = 0 ) ] ] ;
attrIndex + + ;
}
// Close second level - attributes
[ eventString appendString : @ "}" ] ;
}
// If there are any report attributes for this event , add them as above
attrIndex = 0 ;
if ( reportAttributes ! = nil )
{
[ eventString appendString : [ NSString stringWithFormat : @ ",\" % @ \ ":{" , PARAM_REPORT _ATTRIBUTES ] ] ;
for ( id key in [ reportAttributes allKeys ] ) {
[ eventString appendString :
[ self formatAttributeWithName : [ self escapeString : [ key description ] ]
value : [ self escapeString : [ [ reportAttributes valueForKey : key ] description ] ]
first : ( attrIndex = = 0 ) ] ] ;
attrIndex + + ;
}
[ eventString appendString : @ "}" ] ;
}
// Close first level - Event information
[ eventString appendString : @ "}\n" ] ;
BOOL success = [ [ LocalyticsDatabase sharedLocalyticsDatabase ] addEventWithBlobString : [ [ eventString copy ] autorelease ] ] ;
if ( success ) {
// User - originated events should be tracked as application flow .
[ self addFlowEventWithName : event type : @ "e" ] ; // "e" for Event .
[ self ampTrigger : event ] ;
[ self logMessage : [ @ "Tagged event: " stringByAppendingString : event ] ] ;
} else {
[ self logMessage : @ "Failed to tag event." ] ;
}
}
@ catch ( NSException * e ) { }
} ) ;
}
- ( void ) 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 ) setLocation : ( CLLocationCoordinate2D ) deviceLocation {
lastDeviceLocation = deviceLocation ;
[ self logMessage : @ "Setting Location" ] ;
}
- ( 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 useHTTPS : [ self enableHTTPS ] installId : [ self installationId ] resultTarget : self callback : @ selector ( uploadCallback : ) ] ;
} else {
[ db rollbackTransaction : t ] ;
[ self logMessage : @ "Failed to start upload." ] ;
}
}
@ catch ( NSException * e ) { }
} ) ;
}
# pragma mark Private Methods
- ( NSString * ) libraryVersion {
return CLIENT_VERSION ;
}
- ( void ) uploadCallback : ( NSDictionary * ) info {
}
- ( void ) dequeueCloseEventBlobString
{
LocalyticsDatabase * db = [ LocalyticsDatabase sharedLocalyticsDatabase ] ;
NSString * closeEventString = [ db dequeueCloseEventBlobString ] ;
if ( closeEventString ) {
BOOL success = [ db addCloseEventWithBlobString : closeEventString ] ;
if ( ! success ) {
// Re - queue the close event .
[ db queueCloseEventWithBlobString : closeEventString ] ;
}
}
}
- ( void ) ll_open {
// There are a number of conditions in which nothing should be done :
if ( self . hasInitialized = = NO || // the session object has not yet initialized
self . isSessionOpen = = YES ) // session has already been opened
{
[ self logMessage : @ "Unable to open session." ] ;
return ;
}
if ( [ self ll_isOptedIn ] = = false ) {
[ self logMessage : @ "Can't open session because user is opted out." ] ;
return ;
}
@ try {
// If there is too much data on the disk , don ' t bother collecting any more .
LocalyticsDatabase * db = [ LocalyticsDatabase sharedLocalyticsDatabase ] ;
if ( [ db databaseSize ] > MAX_DATABASE _SIZE ) {
[ self logMessage : @ "Database has exceeded the maximum size. Session not opened." ] ;
self . isSessionOpen = NO ;
return ;
}
[ self dequeueCloseEventBlobString ] ;
self . sessionActiveDuration = 0 ;
self . sessionResumeTime = [ NSDate date ] ;
self . unstagedFlowEvents = [ NSMutableString string ] ;
self . stagedFlowEvents = [ NSMutableString string ] ;
self . screens = [ NSMutableString string ] ;
// Begin transaction for session open .
NSString * t = @ "open_session" ;
BOOL success = [ db beginTransaction : t ] ;
// lastSessionStartTimestamp isn ' t really the last session start time .
// It ' s the sessionResumeTime which is [ NSDate date ] or now . Therefore ,
// save the current lastSessionTimestamp value from the database so it
// can be used to calculate the elapsed time between session start times .
NSTimeInterval previousSessionStartTimeInterval = [ db lastSessionStartTimestamp ] ;
// Save session start time .
self . lastSessionStartTimestamp = [ self . sessionResumeTime timeIntervalSince1970 ] ;
if ( success ) {
success = [ db setLastsessionStartTimestamp : self . lastSessionStartTimestamp ] ;
}
// Retrieve next session number .
int sessionNumber = 0 ;
if ( success ) {
success = [ db incrementLastSessionNumber : & sessionNumber ] ;
}
[ self setSessionNumber : sessionNumber ] ;
if ( success ) {
// Prepare session open event .
self . sessionUUID = [ self randomUUID ] ;
// Store event .
NSMutableString * openEventString = [ NSMutableString string ] ;
[ openEventString appendString : @ "{" ] ;
[ openEventString appendString : [ self formatAttributeWithName : PARAM_DATA _TYPE value : @ "s" first : YES ] ] ;
[ openEventString appendString : [ self formatAttributeWithName : PARAM_NEW _SESSION _UUID value : self . sessionUUID ] ] ;
[ openEventString appendFormat : @ ",\" % @ \ ":%ld" , PARAM_CLIENT _TIME , ( long ) self . lastSessionStartTimestamp ] ;
[ openEventString appendFormat : @ ",\" % @ \ ":%d" , PARAM_SESSION _NUMBER , sessionNumber ] ;
double elapsedTime = 0.0 ;
if ( previousSessionStartTimeInterval > 0 ) {
elapsedTime = [ self lastSessionStartTimestamp ] - previousSessionStartTimeInterval ;
}
NSString * elapsedTimeString = [ NSString stringWithFormat : @ "%.0f" , elapsedTime ] ;
[ openEventString appendString : [ self formatAttributeWithName : PARAM_SESSION _ELAPSE _TIME value : elapsedTimeString ] ] ;
[ openEventString appendString : [ self customDimensions ] ] ;
[ openEventString appendString : [ self locationDimensions ] ] ;
[ openEventString appendString : @ "}\n" ] ;
[ self customDimensions ] ;
success = [ db addEventWithBlobString : [ [ openEventString copy ] autorelease ] ] ;
}
if ( success ) {
[ db releaseTransaction : t ] ;
self . isSessionOpen = YES ;
self . sessionHasBeenOpen = YES ;
[ self logMessage : [ @ "Succesfully opened session. UUID is: " stringByAppendingString : self . sessionUUID ] ] ;
} else {
[ db rollbackTransaction : t ] ;
self . isSessionOpen = NO ;
[ self logMessage : @ "Failed to open session." ] ;
}
}
@ catch ( NSException * e ) { }
}
/ * !
@ 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 ] ;
NSString * device_adid = [ self advertisingIdentifier ] ;
// Open first level - blob information
[ headerString appendString : @ "{" ] ;
[ headerString appendFormat : @ "\" % @ \ ":%d" , PARAM_SEQUENCE _NUMBER , nextSequenceNumber ] ;
[ headerString appendFormat : @ ",\" % @ \ ":%ld" , PARAM_PERSISTED _AT , ( long ) [ [ LocalyticsDatabase sharedLocalyticsDatabase ] createdTimestamp ] ] ;
[ headerString appendString : [ self formatAttributeWithName : PARAM_DATA _TYPE value : @ "h" ] ] ;
[ headerString appendString : [ self formatAttributeWithName : PARAM_UUID value : uuid ] ] ;
// Open second level - blob header attributes
[ headerString appendString : [ NSString stringWithFormat : @ ",\" % @ \ ":{" , PARAM_ATTRIBUTES ] ] ;
[ headerString appendString : [ self formatAttributeWithName : PARAM_DATA _TYPE value : @ "a" first : YES ] ] ;
// > > 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 : [ self libraryVersion ] ] ] ;
// > > Device Information
if ( device_uuid ) {
[ headerString appendString : [ self formatAttributeWithName : PARAM_DEVICE _UUID _HASHED value : [ self hashString : device_uuid ] ] ] ;
}
if ( device_adid ) {
[ headerString appendString : [ self formatAttributeWithName : PARAM_DEVICE _ADID value : device_adid ] ] ;
}
[ headerString appendString : [ self formatAttributeWithName : PARAM_DEVICE _PLATFORM value : [ thisDevice model ] ] ] ;
[ headerString appendString : [ self formatAttributeWithName : PARAM_DEVICE _OS _VERSION value : [ thisDevice systemVersion ] ] ] ;
[ headerString appendString : [ self formatAttributeWithName : PARAM_DEVICE _MODEL value : [ self deviceModel ] ] ] ;
// MAC Address collection . Uncomment the following line to add Mac address to the mix of collected identifiers
// [ headerString appendString : [ self formatAttributeWithName : PARAM_DEVICE _MAC value : [ self hashString : [ self macAddress ] ] ] ] ;
[ headerString appendString : [ NSString stringWithFormat : @ ",\" % @ \ ":%ld" , PARAM_DEVICE _MEMORY , ( long ) [ self availableMemory ] ] ] ;
[ headerString appendString : [ 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_UUID value : [ self randomUUID ] first : NO ] ] ;
[ optEventString appendString : [ NSString stringWithFormat : @ ",\" % @ \ ":%@" , PARAM_OPT _VALUE , ( optState ? @ "false" : @ "true" ) ] ] ; // this actually transmits the opposite of the opt state . The JSON contains whether the user is opted out , not whether the user is opted in .
[ optEventString appendFormat : @ ",\" % @ \ ":%ld" , PARAM_CLIENT _TIME , ( long ) [ self currentTimestamp ] ] ;
[ optEventString appendString : @ "}\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 : @ ",\" % @ \ ":%ld" , PARAM_SESSION _START , ( long ) self . lastSessionStartTimestamp ] ;
// Open second level - new flow events
[ flowEventString appendFormat : @ ",\" % @ \ ":[" , PARAM_NEW _FLOW _EVENTS ] ;
[ flowEventString appendString : self . unstagedFlowEvents ] ; // Flow events are escaped in | - addFlowEventWithName : |
// Close second level - new flow events
[ flowEventString appendString : @ "]" ] ;
// Open second level - old flow events
[ flowEventString appendFormat : @ ",\" % @ \ ":[" , PARAM_OLD _FLOW _EVENTS ] ;
[ flowEventString appendString : self . stagedFlowEvents ] ;
// Close second level - old flow events
[ flowEventString appendString : @ "]" ] ;
// Close first level - flow blob event
[ flowEventString appendString : @ "}\n" ] ;
success = [ [ LocalyticsDatabase sharedLocalyticsDatabase ] addFlowEventWithBlobString : [ [ flowEventString copy ] autorelease ] ] ;
}
return success ;
}
// 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 : @ "\'" withString : @ "\\\'" ] ;
output = [ output stringByReplacingOccurrencesOfString : @ "\n" withString : @ "\\n" ] ;
output = [ output stringByReplacingOccurrencesOfString : @ "\t" withString : @ "\\t" ] ;
output = [ output stringByReplacingOccurrencesOfString : @ "\b" withString : @ "\\b" ] ;
output = [ output stringByReplacingOccurrencesOfString : @ "\r" withString : @ "\\r" ] ;
output = [ output stringByReplacingOccurrencesOfString : @ "\f" withString : @ "\\f" ] ;
output = [ output stringByReplacingOccurrencesOfString : @ "\v" withString : @ "\\v" ] ;
return output ;
}
- ( 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 locationDimensions
@ abstract Returns the json blob containing the current location if available or nil if no location is available .
* /
- ( NSString * ) locationDimensions
{
if ( lastDeviceLocation . latitude = = 0 || lastDeviceLocation . longitude = = 0 ) {
return @ "" ;
}
return [ NSString stringWithFormat : @ ",\" lat \ ":%f,\" lng \ ":%f" ,
lastDeviceLocation . latitude ,
lastDeviceLocation . longitude ] ;
return [ NSString stringWithFormat : @ "%lf" , lastDeviceLocation . latitude ] ;
}
/ * !
@ method macAddress
@ abstract Returns the macAddress of this device .
* /
- ( NSString * ) macAddress
{
NSMutableString * result = [ NSMutableString string ] ;
BOOL success ;
struct ifaddrs * addrs ;
const struct ifaddrs * cursor ;
const struct sockaddr_dl * dlAddr ;
const uint8_t * base ;
int i ;
success = ( getifaddrs ( & addrs ) = = 0 ) ;
if ( success )
{
cursor = addrs ;
while ( cursor ! = NULL )
{
if ( ( cursor -> ifa_addr -> sa_family = = AF_LINK ) && ( ( ( const struct sockaddr_dl * ) cursor -> ifa_addr ) -> sdl_type = = IFT_ETHER ) )
{
dlAddr = ( const struct sockaddr_dl * ) cursor -> ifa_addr ;
base = ( const uint8_t * ) & dlAddr -> sdl_data [ dlAddr -> sdl_nlen ] ;
for ( i = 0 ; 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 {
NSString * systemId = nil ;
// We collect it as long as it is available along with a randomly generated ID .
// This way , when this becomes unavailable we can map existing users so the
// new vs returning counts do not break .
// only do this if the OS is less than 6.0
if ( ( [ [ [ UIDevice currentDevice ] systemVersion ] floatValue ] < 6.0 f ) ) {
SEL udidSelector = NSSelectorFromString ( @ "uniqueIdentifier" ) ;
if ( [ [ UIDevice currentDevice ] respondsToSelector : udidSelector ] ) {
systemId = [ [ UIDevice currentDevice ] performSelector : udidSelector ] ;
}
}
return systemId ;
}
/ * !
@ method advertisingIdentifier
@ abstract An alphanumeric string unique to each device , used for advertising only .
From UIDevice documentation .
@ return An identifier unique to this device .
* /
- ( NSString * ) advertisingIdentifier {
NSString * adId = nil ;
2012-09-21 17:42:18 +00:00
Class advertisingClass = NSClassFromString ( @ "ASIdentifierManager" ) ;
if ( advertisingClass ) {
SEL adidSelector = NSSelectorFromString ( @ "advertisingIdentifier" ) ;
adId = [ [ [ advertisingClass performSelector : NSSelectorFromString ( @ "sharedManager" ) ] performSelector : adidSelector ] performSelector : NSSelectorFromString ( @ "UUIDString" ) ] ;
}
2012-09-12 13:58:54 +00:00
return adId ;
}
/ * !
@ method appVersion
@ abstract Gets the pretty string for this application ' s version .
@ return The application ' s version as a pretty string
* /
- ( NSString * ) appVersion {
return [ [ [ NSBundle mainBundle ] infoDictionary ] objectForKey : @ "CFBundleVersion" ] ;
}
/ * !
@ 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 . 8 GB , 16 GB , 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 ] ;
}
# pragma mark - AMP stub
- ( void ) ampTrigger : ( NSString * ) event {
// do nothing
}
@ end