diff --git a/Adjust/AIActivityHandler.h b/Adjust/AIActivityHandler.h index e8a5c65c7..d71ff81d1 100644 --- a/Adjust/AIActivityHandler.h +++ b/Adjust/AIActivityHandler.h @@ -30,6 +30,8 @@ withParameters:(NSDictionary *)parameters; - (void)finishedTrackingWithResponse:(AIResponseData *)response; +- (void)setEnabled:(BOOL)enabled; +- (BOOL)isEnabled; @end diff --git a/Adjust/AIActivityHandler.m b/Adjust/AIActivityHandler.m index c322d6294..ae02c5ed8 100644 --- a/Adjust/AIActivityHandler.m +++ b/Adjust/AIActivityHandler.m @@ -42,6 +42,7 @@ @interface AIActivityHandler() @property (nonatomic, copy) NSString *userAgent; @property (nonatomic, copy) NSString *clientSdk; @property (nonatomic, assign) BOOL trackingEnabled; +@property (nonatomic, assign) BOOL internalEnabled; @end @@ -70,6 +71,7 @@ - (id)initWithAppToken:(NSString *)yourAppToken { // default values self.environment = @"unknown"; self.trackMacMd5 = YES; + self.internalEnabled = YES; dispatch_async(self.internalQueue, ^{ [self initInternal:yourAppToken]; @@ -119,6 +121,26 @@ - (void)finishedTrackingWithResponse:(AIResponseData *)response { } } +- (void)setEnabled:(BOOL)enabled { + self.internalEnabled = enabled; + if ([self checkActivityState:self.activityState]) { + self.activityState.enabled = enabled; + } + if (enabled) { + [self trackSubsessionStart]; + } else { + [self trackSubsessionEnd]; + } +} + +- (BOOL)isEnabled { + if ([self checkActivityState:self.activityState]) { + return self.activityState.enabled; + } else { + return self.internalEnabled; + } +} + #pragma mark - internal - (void)initInternal:(NSString *)yourAppToken { if (![self checkAppTokenNotNil:yourAppToken]) return; @@ -144,6 +166,11 @@ - (void)initInternal:(NSString *)yourAppToken { - (void)startInternal { if (![self checkAppTokenNotNil:self.appToken]) return; + if (self.activityState != nil + && !self.activityState.enabled) { + return; + } + [self.packageHandler resumeSending]; [self startTimer]; @@ -157,6 +184,7 @@ - (void)startInternal { [self transferSessionPackage]; [self.activityState resetSessionAttributes:now]; + self.activityState.enabled = self.internalEnabled; [self writeActivityState]; [self.logger info:@"First session"]; return; @@ -200,7 +228,8 @@ - (void)endInternal { [self.packageHandler pauseSending]; [self stopTimer]; - [self updateActivityState]; + double now = [NSDate.date timeIntervalSince1970]; + [self updateActivityState:now]; [self writeActivityState]; } @@ -212,12 +241,16 @@ - (void)eventInternal:(NSString *)eventToken if (![self checkEventTokenNotNil:eventToken]) return; if (![self checkEventTokenLength:eventToken]) return; + if (!self.activityState.enabled) { + return; + } + AIPackageBuilder *eventBuilder = [[AIPackageBuilder alloc] init]; eventBuilder.eventToken = eventToken; eventBuilder.callbackParameters = parameters; double now = [NSDate.date timeIntervalSince1970]; - [self updateActivityState]; + [self updateActivityState:now]; self.activityState.createdAt = now; self.activityState.eventCount++; @@ -247,13 +280,17 @@ - (void)revenueInternal:(double)amount if (![self checkEventTokenLength:eventToken]) return; if (![self checkTransactionId:transactionId]) return; + if (!self.activityState.enabled) { + return; + } + AIPackageBuilder *revenueBuilder = [[AIPackageBuilder alloc] init]; revenueBuilder.amountInCents = amount; revenueBuilder.eventToken = eventToken; revenueBuilder.callbackParameters = parameters; double now = [NSDate.date timeIntervalSince1970]; - [self updateActivityState]; + [self updateActivityState:now]; self.activityState.createdAt = now; self.activityState.eventCount++; @@ -275,10 +312,9 @@ - (void)revenueInternal:(double)amount #pragma mark - private // returns whether or not the activity state should be written -- (BOOL)updateActivityState { +- (BOOL)updateActivityState:(double)now { if (![self checkActivityState:self.activityState]) return NO; - double now = [NSDate.date timeIntervalSince1970]; double lastInterval = now - self.activityState.lastActivity; if (lastInterval < 0) { [self.logger error:@"Time travel!"]; @@ -375,8 +411,13 @@ - (void)stopTimer { } - (void)timerFired { + if (self.activityState != nil + && !self.activityState.enabled) { + return; + } [self.packageHandler sendFirstPackage]; - if ([self updateActivityState]) { + double now = [NSDate.date timeIntervalSince1970]; + if ([self updateActivityState:now]) { [self writeActivityState]; } } diff --git a/Adjust/AIActivityState.h b/Adjust/AIActivityState.h index d689af5b0..31b9637f2 100644 --- a/Adjust/AIActivityState.h +++ b/Adjust/AIActivityState.h @@ -13,6 +13,7 @@ // persistent data @property (nonatomic, copy) NSString *uuid; +@property (nonatomic, assign) BOOL enabled; // global counters @property (nonatomic, assign) int eventCount; diff --git a/Adjust/AIActivityState.m b/Adjust/AIActivityState.m index adbafc120..9cb97787c 100644 --- a/Adjust/AIActivityState.m +++ b/Adjust/AIActivityState.m @@ -32,6 +32,7 @@ - (id)init { self.createdAt = -1; self.lastInterval = -1; self.transactionIds = [NSMutableArray arrayWithCapacity:kTransactionIdCount]; + self.enabled = YES; return self; } @@ -92,6 +93,7 @@ - (id)initWithCoder:(NSCoder *)decoder { self.lastActivity = [decoder decodeDoubleForKey:@"lastActivity"]; self.uuid = [decoder decodeObjectForKey:@"uuid"]; self.transactionIds = [decoder decodeObjectForKey:@"transactionIds"]; + self.enabled = [decoder decodeBoolForKey:@"enabled"]; // create UUID for migrating devices if (self.uuid == nil) { @@ -102,6 +104,10 @@ - (id)initWithCoder:(NSCoder *)decoder { self.transactionIds = [NSMutableArray arrayWithCapacity:kTransactionIdCount]; } + if (![decoder containsValueForKey:@"enabled"]) { + self.enabled = YES; + } + self.lastInterval = -1; return self; @@ -117,6 +123,7 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeDouble:self.lastActivity forKey:@"lastActivity"]; [encoder encodeObject:self.uuid forKey:@"uuid"]; [encoder encodeObject:self.transactionIds forKey:@"transactionIds"]; + [encoder encodeBool:self.enabled forKey:@"enabled"]; } diff --git a/Adjust/AIPackageBuilder.m b/Adjust/AIPackageBuilder.m index 35fbe1d96..6b02d9eaf 100644 --- a/Adjust/AIPackageBuilder.m +++ b/Adjust/AIPackageBuilder.m @@ -9,9 +9,7 @@ #import "AIPackageBuilder.h" #import "AIActivityPackage.h" #import "NSData+AIAdditions.h" - -static NSString * const kDateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'Z"; -static NSDateFormatter * dateFormat; +#import "AIUtil.h" #pragma mark - @implementation AIPackageBuilder @@ -129,8 +127,7 @@ - (void)parameters:(NSMutableDictionary *)parameters setInt:(int)value forKey:(N - (void)parameters:(NSMutableDictionary *)parameters setDate:(double)value forKey:(NSString *)key { if (value < 0) return; - NSDate *date = [NSDate dateWithTimeIntervalSince1970:value]; - NSString *dateString = [self.dateFormat stringFromDate:date]; + NSString *dateString = [AIUtil dateFormat:value]; [self parameters:parameters setString:dateString forKey:key]; } @@ -149,13 +146,5 @@ - (void)parameters:(NSMutableDictionary *)parameters setDictionary:(NSDictionary [self parameters:parameters setString:dictionaryString forKey:key]; } -- (NSDateFormatter *)dateFormat { - if (dateFormat == nil) { - dateFormat = [[NSDateFormatter alloc] init]; - [dateFormat setDateFormat:kDateFormat]; - } - return dateFormat; -} - @end diff --git a/Adjust/AIRequestHandler.m b/Adjust/AIRequestHandler.m index f5e4a4259..f85faca73 100644 --- a/Adjust/AIRequestHandler.m +++ b/Adjust/AIRequestHandler.m @@ -117,6 +117,12 @@ - (NSData *)bodyForParameters:(NSDictionary *)parameters { [pairs addObject:pair]; } + double now = [NSDate.date timeIntervalSince1970]; + NSString *dateString = [AIUtil dateFormat:now]; + NSString *escapedDate = [dateString aiUrlEncode]; + NSString *sentAtPair = [NSString stringWithFormat:@"%@=%@", @"sent_at", escapedDate]; + [pairs addObject:sentAtPair]; + NSString *bodyString = [pairs componentsJoinedByString:@"&"]; NSData *body = [NSData dataWithBytes:bodyString.UTF8String length:bodyString.length]; return body; diff --git a/Adjust/AIUtil.h b/Adjust/AIUtil.h index a6f442ae8..5f3accd20 100644 --- a/Adjust/AIUtil.h +++ b/Adjust/AIUtil.h @@ -14,5 +14,6 @@ + (NSString *)userAgent; + (void)excludeFromBackup:(NSString *)filename; ++ (NSString *)dateFormat:(double)value; @end diff --git a/Adjust/AIUtil.m b/Adjust/AIUtil.m index a494b317b..ac5ddd344 100644 --- a/Adjust/AIUtil.m +++ b/Adjust/AIUtil.m @@ -16,6 +16,9 @@ static NSString * const kBaseUrl = @"https://app.adjust.io"; static NSString * const kClientSdk = @"ios3.1.0"; +static NSString * const kDateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'Z"; +static NSDateFormatter * dateFormat; + #pragma mark - @implementation AIUtil @@ -110,4 +113,15 @@ + (void)excludeFromBackup:(NSString *)path { } } ++ (NSString *)dateFormat:(double) value { + if (dateFormat == nil) { + dateFormat = [[NSDateFormatter alloc] init]; + [dateFormat setDateFormat:kDateFormat]; + } + + NSDate *date = [NSDate dateWithTimeIntervalSince1970:value]; + + return [dateFormat stringFromDate:date]; +} + @end diff --git a/Adjust/Adjust.h b/Adjust/Adjust.h index 21d57d8c6..71db0b850 100644 --- a/Adjust/Adjust.h +++ b/Adjust/Adjust.h @@ -152,6 +152,18 @@ static NSString * const AIEnvironmentProduction = @"production"; */ + (void)trackSubsessionEnd; +/** + * Enable or disable the adjust SDK + * + * @param enabled The flag to enable or disable the adjust SDK + */ ++ (void)setEnabled:(BOOL)enabled; + +/** + * Check if the SDK is enabled or disabled + */ ++ (BOOL)isEnabled; + @end diff --git a/Adjust/Adjust.m b/Adjust/Adjust.m index 3fc29f9e3..c82d51d83 100644 --- a/Adjust/Adjust.m +++ b/Adjust/Adjust.m @@ -124,4 +124,12 @@ + (void)trackSubsessionEnd { [activityHandler trackSubsessionEnd]; } ++ (void)setEnabled:(BOOL)enabled { + [activityHandler setEnabled:enabled]; +} + ++ (BOOL)isEnabled { + return [activityHandler isEnabled]; +} + @end diff --git a/AdjustTests/AIActivityHandlerMock.m b/AdjustTests/AIActivityHandlerMock.m index 973ec52c9..693f9b0c1 100644 --- a/AdjustTests/AIActivityHandlerMock.m +++ b/AdjustTests/AIActivityHandlerMock.m @@ -63,6 +63,13 @@ - (void)finishedTrackingWithResponse:(AIResponseData *)response { [self.loggerMock test:[prefix stringByAppendingFormat:@"finishedTrackingWithResponse response:%@", response]]; } +- (void)setEnabled:(BOOL)enabled { + [self.loggerMock test:[prefix stringByAppendingFormat:@"setEnabled enabled:%d", enabled]]; +} +- (BOOL)isEnabled { + [self.loggerMock test:[prefix stringByAppendingFormat:@"isEnabled"]]; + return YES; +} @end diff --git a/AdjustTests/AIActivityHandlerTests.m b/AdjustTests/AIActivityHandlerTests.m index d754faee6..a563e7873 100644 --- a/AdjustTests/AIActivityHandlerTests.m +++ b/AdjustTests/AIActivityHandlerTests.m @@ -416,4 +416,80 @@ - (void)testChecks { } +- (void)testDisable { + // reseting to make the test order independent + [self reset]; + + // starting from a clean slate + XCTAssert([AITestsUtil deleteFile:@"AdjustIoActivityState" logger:self.loggerMock], @"%@", self.loggerMock); + + // create handler to start the session + id activityHandler = [AIAdjustFactory activityHandlerWithAppToken:@"123456789012"]; + + // verify the default value + XCTAssert([activityHandler isEnabled], @"%@", self.loggerMock); + + [activityHandler setEnabled:NO]; + + // check that the value is changed + XCTAssertFalse([activityHandler isEnabled], @"%@", self.loggerMock); + + [activityHandler trackEvent:@"123456" withParameters:nil]; + [activityHandler trackRevenue:0.1 transactionId:nil forEvent:nil withParameters:nil]; + [activityHandler trackSubsessionEnd]; + [activityHandler trackSubsessionStart]; + + [NSThread sleepForTimeInterval:2]; + + // verify the changed value after the activity handler is started + XCTAssertFalse([activityHandler isEnabled], @"%@", self.loggerMock); + + // making sure the first session was sent + XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"First session"], @"%@", self.loggerMock); + + // delete the first session package from the log + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], @"%@", self.loggerMock); + + // making sure the timer fired did not call the package handler + XCTAssertFalse([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], @"%@", self.loggerMock); + + // test if the event was not triggered + XCTAssertFalse([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 1"], @"%@", self.loggerMock); + + // test if the revenue was not triggered + XCTAssertFalse([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 1 (revenue)"], @"%@", self.loggerMock); + + // verify that the application was paused + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], @"%@", self.loggerMock); + + // verify that it was not resumed + XCTAssertFalse([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], @"%@", self.loggerMock); + + // enable again + [activityHandler setEnabled:YES]; + + [activityHandler trackEvent:@"123456" withParameters:nil]; + [activityHandler trackRevenue:0.1 transactionId:nil forEvent:nil withParameters:nil]; + [activityHandler trackSubsessionEnd]; + [activityHandler trackSubsessionStart]; + + [NSThread sleepForTimeInterval:2]; + + // verify the changed value, when the activity state is started + XCTAssert([activityHandler isEnabled], @"%@", self.loggerMock); + + // test that the event was triggered + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 1"], @"%@", self.loggerMock); + + // test that the revenue was triggered + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 2 (revenue)"], @"%@", self.loggerMock); + + // verify that the application was paused + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], @"%@", self.loggerMock); + + // verify that it was also resumed + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], @"%@", self.loggerMock); + +} + @end diff --git a/README.md b/README.md index 83ab85a6a..c4a4963b1 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,20 @@ in the `didFinishLaunching` method of your Application Delegate: [Adjust setEventBufferingEnabled:YES]; ``` +### 10. Disable tracking + +You can disable the adjust SDK from tracking by invoking the method `setEnabled` +with the enabled parameter as `NO`. This setting is remembered between sessions, but it can only +be activated after the first session. + +```objc +[Adjust setEnabled:NO]; +``` + +You can verify if the adjust SDK is currently active with the method `isEnabled`. It is always possible +to activate the adjust SDK by invoking `setEnabled` with the enabled parameter as `YES`. + + [adjust.io]: http://adjust.io [cocoapods]: http://cocoapods.org [dashboard]: http://adjust.io diff --git a/VERSION b/VERSION index fd2a01863..944880fa1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 +3.2.0