/* * Copyright 2017 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 "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h" #import "FirebaseInAppMessaging/Sources/Private/Analytics/FIRIAMClearcutLogger.h" #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMFetchFlow.h" #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMRuntimeManager.h" @implementation FIRIAMFetchSetting @end // the notification message to say that the fetch flow is done NSString *const kFIRIAMFetchIsDoneNotification = @"FIRIAMFetchIsDoneNotification"; @interface FIRIAMFetchFlow () @property(nonatomic) id timeFetcher; @property(nonatomic) NSTimeInterval lastFetchTime; @property(nonatomic, nonnull, readonly) FIRIAMFetchSetting *setting; @property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache; @property(nonatomic) id messageFetcher; @property(nonatomic, nonnull, readonly) id fetchBookKeeper; @property(nonatomic, nonnull, readonly) FIRIAMActivityLogger *activityLogger; @property(nonatomic, nonnull, readonly) id analyticsEventLogger; @property(nonatomic, nonnull, readonly) FIRIAMSDKModeManager *sdkModeManager; @property(nonatomic, nonnull, readonly) FIRIAMDisplayExecutor *displayExecutor; @end @implementation FIRIAMFetchFlow - (instancetype)initWithSetting:(FIRIAMFetchSetting *)setting messageCache:(FIRIAMMessageClientCache *)cache messageFetcher:(id)messageFetcher timeFetcher:(id)timeFetcher bookKeeper:(id)fetchBookKeeper activityLogger:(FIRIAMActivityLogger *)activityLogger analyticsEventLogger:(id)analyticsEventLogger FIRIAMSDKModeManager:(FIRIAMSDKModeManager *)sdkModeManager displayExecutor:(FIRIAMDisplayExecutor *)displayExecutor { if (self = [super init]) { _timeFetcher = timeFetcher; _lastFetchTime = [fetchBookKeeper lastFetchTime]; _setting = setting; _messageCache = cache; _messageFetcher = messageFetcher; _fetchBookKeeper = fetchBookKeeper; _activityLogger = activityLogger; _analyticsEventLogger = analyticsEventLogger; _sdkModeManager = sdkModeManager; _displayExecutor = displayExecutor; } return self; } - (FIRIAMAnalyticsLogEventType)fetchErrorToLogEventType:(NSError *)error { if ([error.domain isEqual:NSURLErrorDomain]) { if (error.code == NSURLErrorNotConnectedToInternet) { return FIRIAMAnalyticsEventFetchAPINetworkError; } else { // error.code could be a non 2xx status code if (error.code > 0) { if (error.code >= 400 && error.code < 500) { return FIRIAMAnalyticsEventFetchAPIClientError; } else { if (error.code >= 500 && error.code < 600) { return FIRIAMAnalyticsEventFetchAPIServerError; } } } } } return FIRIAMAnalyticsLogEventUnknown; } - (void)sendFetchIsDoneNotification { [[NSNotificationCenter defaultCenter] postNotificationName:kFIRIAMFetchIsDoneNotification object:self]; } - (void)handleSuccessullyFetchedMessages:(NSArray *)messagesInResponse withFetchWaitTime:(NSNumber *_Nullable)fetchWaitTime requestImpressions:(NSArray *)requestImpressions { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700004", @"%lu messages were fetched successfully.", (unsigned long)messagesInResponse.count); for (FIRIAMMessageDefinition *next in messagesInResponse) { if (next.isTestMessage && self.sdkModeManager.currentMode != FIRIAMSDKModeTesting) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700006", @"Seeing test message in fetch response. Turn " "the current instance into a testing instance."); [self.sdkModeManager becomeTestingInstance]; } } NSArray *responseMessageIDs = [messagesInResponse valueForKeyPath:@"renderData.messageID"]; NSArray *impressionMessageIDs = [requestImpressions valueForKey:@"messageID"]; // We are going to clear impression records for those IDs that are in both impressionMessageIDs // and responseMessageIDs. This is to avoid incorrectly clearing impressions records that come // in between the sending the request and receiving the response for the fetch operation. // So we are computing intersection between responseMessageIDs and impressionMessageIDs and use // that for impression log clearing. NSMutableSet *idIntersection = [NSMutableSet setWithArray:responseMessageIDs]; [idIntersection intersectSet:[NSSet setWithArray:impressionMessageIDs]]; [self.fetchBookKeeper clearImpressionsWithMessageList:[idIntersection allObjects]]; [self.messageCache setMessageData:messagesInResponse]; [self.sdkModeManager registerOneMoreFetch]; [self.fetchBookKeeper recordNewFetchWithFetchCount:messagesInResponse.count withTimestampInSeconds:[self.timeFetcher currentTimestampInSeconds] nextFetchWaitTime:fetchWaitTime]; } - (void)checkAndFetchForInitialAppLaunch:(BOOL)forInitialAppLaunch { NSTimeInterval intervalFromLastFetchInSeconds = [self.timeFetcher currentTimestampInSeconds] - self.fetchBookKeeper.lastFetchTime; FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700005", @"Interval from last time fetch is %lf seconds", intervalFromLastFetchInSeconds); BOOL fetchIsAllowedNow = NO; if (intervalFromLastFetchInSeconds >= self.fetchBookKeeper.nextFetchWaitTime) { // it's enough wait time interval from last fetch. fetchIsAllowedNow = YES; } else { FIRIAMSDKMode sdkMode = [self.sdkModeManager currentMode]; if (sdkMode == FIRIAMSDKModeNewlyInstalled || sdkMode == FIRIAMSDKModeTesting) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700007", @"OK to fetch due to current SDK mode being %@", FIRIAMDescriptonStringForSDKMode(sdkMode)); fetchIsAllowedNow = YES; } else { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700008", @"Interval from last time fetch is %lf seconds, smaller than fetch wait time %lf", intervalFromLastFetchInSeconds, self.fetchBookKeeper.nextFetchWaitTime); } } if (fetchIsAllowedNow) { // we are allowed to fetch in-app message from time interval wise FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch isSuccessful:YES withDetail:@"OK to do a fetch" timestamp:nil]; [self.activityLogger addLogRecord:record]; NSArray *impressions = [self.fetchBookKeeper getImpressions]; FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700001", @"Go ahead to fetch messages"); NSTimeInterval fetchStartTime = [[NSDate date] timeIntervalSince1970]; [self.messageFetcher fetchMessagesWithImpressionList:impressions withCompletion:^(NSArray *_Nullable messages, NSNumber *_Nullable nextFetchWaitTime, NSInteger discardedMessageCount, NSError *_Nullable error) { if (error) { FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM700002", @"Error happened during message fetching %@", error); FIRIAMAnalyticsLogEventType eventType = [self fetchErrorToLogEventType:error]; [self.analyticsEventLogger logAnalyticsEventForType:eventType forCampaignID:@"all" withCampaignName:@"all" eventTimeInMs:nil completion:^(BOOL success){ // nothing to do }]; FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeFetchMessage isSuccessful:NO withDetail:error.description timestamp:nil]; [self.activityLogger addLogRecord:record]; } else { double fetchOperationLatencyInMills = ([[NSDate date] timeIntervalSince1970] - fetchStartTime) * 1000; NSString *impressionListString = [impressions componentsJoinedByString:@","]; NSString *activityLogDetail = @""; if (discardedMessageCount > 0) { activityLogDetail = [NSString stringWithFormat: @"%lu messages fetched with impression list as [%@]" " and %lu messages are discarded due to data being " "invalid. It took" " %lf milliseconds", (unsigned long)messages.count, impressionListString, (unsigned long)discardedMessageCount, fetchOperationLatencyInMills]; } else { activityLogDetail = [NSString stringWithFormat: @"%lu messages fetched with impression list as [%@]. It took" " %lf milliseconds", (unsigned long)messages.count, impressionListString, fetchOperationLatencyInMills]; } FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeFetchMessage isSuccessful:YES withDetail:activityLogDetail timestamp:nil]; [self.activityLogger addLogRecord:record]; // Now handle the fetched messages. [self handleSuccessullyFetchedMessages:messages withFetchWaitTime:nextFetchWaitTime requestImpressions:impressions]; if (forInitialAppLaunch) { [self checkForAppLaunchMessage]; } } // Send this regardless whether fetch is successful or not. [self sendFetchIsDoneNotification]; }]; } else { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700003", @"Only %lf seconds from last fetch time. No action.", intervalFromLastFetchInSeconds); // for no fetch case, we still send out the notification so that and display flow can continue // from here. [self sendFetchIsDoneNotification]; FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch isSuccessful:NO withDetail:@"Abort due to check time interval " "not reached yet" timestamp:nil]; [self.activityLogger addLogRecord:record]; } } - (void)checkForAppLaunchMessage { [self.displayExecutor checkAndDisplayNextAppLaunchMessage]; } @end #endif // TARGET_OS_IOS || TARGET_OS_TV