/* * Copyright 2018 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import #if TARGET_OS_IOS || TARGET_OS_TV #import #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h" #import "FirebaseInAppMessaging/Sources/Private/Analytics/FIRIAMClearcutUploader.h" #import "FirebaseInAppMessaging/Sources/Private/Util/FIRIAMTimeFetcher.h" #import "FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutHttpRequestSender.h" #import "FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.h" // a macro for turning a millisecond value into seconds #define MILLS_TO_SECONDS(x) (((long)x) / 1000) @implementation FIRIAMClearcutStrategy - (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills batchSendSize:(NSInteger)batchSendSize { if (self = [super init]) { _minimalWaitTimeInMills = minWaitTimeInMills; _maximumWaitTimeInMills = maxWaitTimeInMills; _failureBackoffTimeInMills = failureBackoffTimeInMills; _batchSendSize = batchSendSize; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"min wait time in seconds:%ld;max wait time in seconds:%ld;" "failure backoff time in seconds:%ld;batch send size:%d", MILLS_TO_SECONDS(self.minimalWaitTimeInMills), MILLS_TO_SECONDS(self.maximumWaitTimeInMills), MILLS_TO_SECONDS(self.failureBackoffTimeInMills), (int)self.batchSendSize]; } @end @interface FIRIAMClearcutUploader () { dispatch_queue_t _queue; BOOL _nextSendScheduled; } @property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender; @property(nonatomic, assign) int64_t nextValidSendTimeInMills; @property(nonatomic, readonly) id timeFetcher; @property(nonatomic, readonly) FIRIAMClearcutLogStorage *logStorage; @property(nonatomic, readonly) FIRIAMClearcutStrategy *strategy; @property(nonatomic, readonly) NSUserDefaults *userDefaults; @end static NSString *FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills = @"firebase-iam-next-clearcut-upload-timestamp-in-mills"; /** * The high level behavior in this implementation is like this * 1 New records always pushed into FIRIAMClearcutLogStorage first. * 2 Upload log records in batches. * 3 If prior upload was successful, next upload would wait for the time parsed out of the * clearcut response body. * 4 If prior upload failed, next upload attempt would wait for failureBackoffTimeInMills defined * in strategy * 5 When app */ @implementation FIRIAMClearcutUploader - (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender timeFetcher:(id)timeFetcher logStorage:(FIRIAMClearcutLogStorage *)logStorage usingStrategy:(FIRIAMClearcutStrategy *)strategy usingUserDefaults:(nullable NSUserDefaults *)userDefaults { if (self = [super init]) { _nextSendScheduled = NO; _timeFetcher = timeFetcher; _requestSender = requestSender; _logStorage = logStorage; _strategy = strategy; _queue = dispatch_queue_create("com.google.firebase.inappmessaging.clearcut_upload", NULL); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scheduleNextSendFromForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; #if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scheduleNextSendFromForeground:) name:UISceneWillEnterForegroundNotification object:nil]; } #endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults]; // it would be 0 if it does not exist, which is equvilent to saying that // you can send now _nextValidSendTimeInMills = (int64_t) [_userDefaults doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills]; NSArray *availableLogs = [logStorage popStillValidRecordsForUpTo:strategy.batchSendSize]; if (availableLogs.count) { [self scheduleNextSend]; } FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260001", @"FIRIAMClearcutUploader created with strategy as %@", self.strategy); } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)scheduleNextSendFromForeground:(NSNotification *)notification { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260010", @"App foregrounded, FIRIAMClearcutUploader will seed next send"); [self scheduleNextSend]; } - (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260002", @"New log record sent to clearcut uploader"); [self.logStorage pushRecords:@[ record ]]; [self scheduleNextSend]; } - (void)attemptUploading { NSArray *availableLogs = [self.logStorage popStillValidRecordsForUpTo:self.strategy.batchSendSize]; if (availableLogs.count > 0) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260011", @"Deliver %d clearcut records", (int)availableLogs.count); [self.requestSender sendClearcutHttpRequestForLogs:availableLogs withCompletion:^(BOOL success, BOOL shouldRetryLogs, int64_t waitTimeInMills) { if (success) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260003", @"Delivering %d clearcut records was successful", (int)availableLogs.count); // make sure the effective wait time is between two bounds // defined in strategy waitTimeInMills = MAX(self.strategy.minimalWaitTimeInMills, waitTimeInMills); waitTimeInMills = MIN(waitTimeInMills, self.strategy.maximumWaitTimeInMills); } else { // failed to deliver FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260004", @"Failed to attempt the delivery of %d clearcut " @"records and should-retry for them is %@", (int)availableLogs.count, shouldRetryLogs ? @"YES" : @"NO"); if (shouldRetryLogs) { /** * Note that there is a chance that the app crashes before we can * call pushRecords: on the logStorage below which means we lost * these log records permanently. This is a trade-off between handling * duplicate records on server side vs taking the risk of lossing * data. This implementation picks the latter. */ FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"Push failed log records back to storage"); [self.logStorage pushRecords:availableLogs]; } waitTimeInMills = (int64_t)self.strategy.failureBackoffTimeInMills; } FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260005", @"Wait for at least %ld seconds before next upload attempt", MILLS_TO_SECONDS(waitTimeInMills)); self.nextValidSendTimeInMills = (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000 + waitTimeInMills; // persisted so that it can be recovered next time the app runs [self.userDefaults setDouble:(double)self.nextValidSendTimeInMills forKey: FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills]; @synchronized(self) { self->_nextSendScheduled = NO; } [self scheduleNextSend]; }]; } else { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"No clearcut records to be uploaded"); @synchronized(self) { _nextSendScheduled = NO; } } } - (void)scheduleNextSend { @synchronized(self) { if (_nextSendScheduled) { return; } } int64_t delayTimeInMills = self.nextValidSendTimeInMills - (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000; if (delayTimeInMills <= 0) { delayTimeInMills = 0; // no need to delay since we can send now } FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260006", @"Next upload attempt scheduled in %d seconds", (int)delayTimeInMills / 1000); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayTimeInMills * (int64_t)NSEC_PER_MSEC), _queue, ^{ [self attemptUploading]; }); @synchronized(self) { _nextSendScheduled = YES; } } @end #endif // TARGET_OS_IOS || TARGET_OS_TV