/* * 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/Extension/FirebaseCoreInternal.h" #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h" #import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMBookKeeper.h" NSString *const FIRIAM_UserDefaultsKeyForImpressions = @"firebase-iam-message-impressions"; NSString *const FIRIAM_UserDefaultsKeyForLastImpressionTimestamp = @"firebase-iam-last-impression-timestamp"; NSString *FIRIAM_UserDefaultsKeyForLastFetchTimestamp = @"firebase-iam-last-fetch-timestamp"; // The two keys used to map FIRIAMImpressionRecord object to a NSDictionary object for // persistence. NSString *const FIRIAM_ImpressionDictKeyForID = @"message_id"; NSString *const FIRIAM_ImpressionDictKeyForTimestamp = @"impression_time"; static NSString *const kUserDefaultsKeyForFetchWaitTime = @"firebase-iam-fetch-wait-time"; // 24 hours static NSTimeInterval kDefaultFetchWaitTimeInSeconds = 24 * 60 * 60; // 3 days static NSTimeInterval kMaxFetchWaitTimeInSeconds = 3 * 24 * 60 * 60; @interface FIRIAMBookKeeperViaUserDefaults () @property(nonatomic) double lastDisplayTime; @property(nonatomic) double lastFetchTime; @property(nonatomic) double nextFetchWaitTime; @property(nonatomic, nonnull) NSUserDefaults *defaults; @end @interface FIRIAMImpressionRecord () - (instancetype)initWithStorageDictionary:(NSDictionary *)dict; @end @implementation FIRIAMImpressionRecord - (instancetype)initWithMessageID:(NSString *)messageID impressionTimeInSeconds:(long)impressionTime { if (self = [super init]) { _messageID = messageID; _impressionTimeInSeconds = impressionTime; } return self; } - (instancetype)initWithStorageDictionary:(NSDictionary *)dict { id timestamp = dict[FIRIAM_ImpressionDictKeyForTimestamp]; id messageID = dict[FIRIAM_ImpressionDictKeyForID]; if (![timestamp isKindOfClass:[NSNumber class]] || ![messageID isKindOfClass:[NSString class]]) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270003", @"Incorrect data in the dictionary object for creating a FIRIAMImpressionRecord" " object"); return nil; } else { return [self initWithMessageID:messageID impressionTimeInSeconds:((NSNumber *)timestamp).longValue]; } } - (NSString *)description { return [NSString stringWithFormat:@"%@ impressed at %ld in seconds", self.messageID, self.impressionTimeInSeconds]; } @end @implementation FIRIAMBookKeeperViaUserDefaults - (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults { if (self = [super init]) { _defaults = userDefaults; // ok if it returns 0 due to the entry being absent _lastDisplayTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp]; _lastFetchTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; id fetchWaitTimeEntry = [_defaults objectForKey:kUserDefaultsKeyForFetchWaitTime]; if (![fetchWaitTimeEntry isKindOfClass:NSNumber.class]) { // This corresponds to the case there is no wait time entry is set in user defaults yet _nextFetchWaitTime = kDefaultFetchWaitTimeInSeconds; } else { _nextFetchWaitTime = ((NSNumber *)fetchWaitTimeEntry).doubleValue; FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270009", @"Next fetch wait time loaded from user defaults is %lf", _nextFetchWaitTime); } } return self; } // A helper function for reading and verifying the stored array data for impressions // in UserDefaults. It returns nil if it does not exist or fail to pass the data type // checking. - (NSArray *)fetchImpressionArrayFromStorage { id impressionsData = [self.defaults objectForKey:FIRIAM_UserDefaultsKeyForImpressions]; if (impressionsData && ![impressionsData isKindOfClass:[NSArray class]]) { FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM270007", @"Found non-array data from impression userdefaults storage with key %@", FIRIAM_UserDefaultsKeyForImpressions); return nil; } return (NSArray *)impressionsData; } - (void)recordNewImpressionForMessage:(NSString *)messageID withStartTimestampInSeconds:(double)timestamp { @synchronized(self) { NSArray *oldImpressions = [self fetchImpressionArrayFromStorage]; // oldImpressions could be nil at the first time NSMutableArray *newImpressions = oldImpressions ? [oldImpressions mutableCopy] : [[NSMutableArray alloc] init]; // Two cases // If a prior impression exists for that messageID, update its impression timestamp // If a prior impression for that messageID does not exist, add a new entry for the // messageID. NSDictionary *newImpressionEntry = @{ FIRIAM_ImpressionDictKeyForID : messageID, FIRIAM_ImpressionDictKeyForTimestamp : [NSNumber numberWithDouble:timestamp] }; BOOL oldImpressionRecordFound = NO; for (int i = 0; i < newImpressions.count; i++) { if ([newImpressions[i] isKindOfClass:[NSDictionary class]]) { NSDictionary *currentItem = (NSDictionary *)newImpressions[i]; if ([messageID isEqualToString:currentItem[FIRIAM_ImpressionDictKeyForID]]) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270001", @"Updating timestamp of existing impression record to be %f for " "message %@", timestamp, messageID); [newImpressions replaceObjectAtIndex:i withObject:newImpressionEntry]; oldImpressionRecordFound = YES; break; } } } if (!oldImpressionRecordFound) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270002", @"Insert the first impression record for message %@ with timestamp in seconds " "as %f", messageID, timestamp); [newImpressions addObject:newImpressionEntry]; } [self.defaults setObject:newImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions]; [self.defaults setDouble:timestamp forKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp]; self.lastDisplayTime = timestamp; } } - (void)clearImpressionsWithMessageList:(NSArray *)messageList { @synchronized(self) { NSArray *existingImpressions = [self fetchImpressionArrayFromStorage]; NSSet *messageIDSet = [NSSet setWithArray:messageList]; NSPredicate *notInMessageListPredicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { if (![evaluatedObject isKindOfClass:[NSDictionary class]]) { return NO; // unexpected item. Throw it away } NSDictionary *impression = (NSDictionary *)evaluatedObject; return impression[FIRIAM_ImpressionDictKeyForID] && ![messageIDSet containsObject:impression[FIRIAM_ImpressionDictKeyForID]]; }]; NSArray *updatedImpressions = [existingImpressions filteredArrayUsingPredicate:notInMessageListPredicate]; if (existingImpressions.count != updatedImpressions.count) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270004", @"Updating the impression records after purging %d items based on the " "server fetch response", (int)(existingImpressions.count - updatedImpressions.count)); [self.defaults setObject:updatedImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions]; } else { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270005", @"No impression records update due to no change after applying the server " "message list"); } } } - (NSArray *)getImpressions { NSArray *impressionsFromStorage = [self fetchImpressionArrayFromStorage]; NSMutableArray *resultArray = [[NSMutableArray alloc] init]; for (NSDictionary *next in impressionsFromStorage) { FIRIAMImpressionRecord *nextImpression = [[FIRIAMImpressionRecord alloc] initWithStorageDictionary:next]; [resultArray addObject:nextImpression]; } return resultArray; } - (NSArray *)getMessageIDsFromImpressions { NSArray *impressionsFromStorage = [self fetchImpressionArrayFromStorage]; NSMutableArray *resultArray = [[NSMutableArray alloc] init]; for (NSDictionary *next in impressionsFromStorage) { [resultArray addObject:next[FIRIAM_ImpressionDictKeyForID]]; } return resultArray; } - (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount withTimestampInSeconds:(double)fetchTimestamp nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime; { [self.defaults setDouble:fetchTimestamp forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; self.lastFetchTime = fetchTimestamp; if (nextFetchWaitTime != nil) { if (nextFetchWaitTime.doubleValue > kMaxFetchWaitTimeInSeconds) { FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM270006", @"next fetch wait time %lf is too large. Ignore it.", nextFetchWaitTime.doubleValue); } else { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270008", @"Setting next fetch wait time as %lf from fetch response.", nextFetchWaitTime.doubleValue); self.nextFetchWaitTime = nextFetchWaitTime.doubleValue; [self.defaults setObject:nextFetchWaitTime forKey:kUserDefaultsKeyForFetchWaitTime]; } } } - (void)cleanupImpressions { [self.defaults setObject:@[] forKey:FIRIAM_UserDefaultsKeyForImpressions]; } - (void)cleanupFetchRecords { [self.defaults setDouble:0 forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; self.lastFetchTime = 0; } @end #endif // TARGET_OS_IOS || TARGET_OS_TV