| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- /*
- * 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 <TargetConditionals.h>
- #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
- #import <GoogleUtilities/GULUserDefaults.h>
- #import <UIKit/UIKit.h>
- #import "FirebaseCore/Extension/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<FIRIAMTimeFetcher> timeFetcher;
- @property(nonatomic, readonly) FIRIAMClearcutLogStorage *logStorage;
- @property(nonatomic, readonly) FIRIAMClearcutStrategy *strategy;
- @property(nonatomic, readonly) GULUserDefaults *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<FIRIAMTimeFetcher>)timeFetcher
- logStorage:(FIRIAMClearcutLogStorage *)logStorage
- usingStrategy:(FIRIAMClearcutStrategy *)strategy
- usingUserDefaults:(nullable GULUserDefaults *)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 (@available(iOS 13.0, tvOS 13.0, *)) {
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(scheduleNextSendFromForeground:)
- name:UISceneWillEnterForegroundNotification
- object:nil];
- }
- _userDefaults = userDefaults ? userDefaults : [GULUserDefaults 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<FIRIAMClearcutLogRecord *> *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<FIRIAMClearcutLogRecord *> *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 losing
- * 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 || TARGET_OS_VISION
|