/* * 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 || TARGET_OS_VISION #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h" #import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMActivityLogger.h" @implementation FIRIAMActivityRecord static NSString *const kActiveTypeArchiveKey = @"type"; static NSString *const kIsSuccessArchiveKey = @"is_success"; static NSString *const kTimeStampArchiveKey = @"timestamp"; static NSString *const kDetailArchiveKey = @"detail"; + (BOOL)supportsSecureCoding { return YES; } - (id)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self != nil) { _activityType = [decoder decodeIntegerForKey:kActiveTypeArchiveKey]; _timestamp = [decoder decodeObjectOfClass:[NSDate class] forKey:kTimeStampArchiveKey]; _success = [decoder decodeBoolForKey:kIsSuccessArchiveKey]; _detail = [decoder decodeObjectOfClass:[NSString class] forKey:kDetailArchiveKey]; } return self; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeInteger:self.activityType forKey:kActiveTypeArchiveKey]; [encoder encodeObject:self.timestamp forKey:kTimeStampArchiveKey]; [encoder encodeBool:self.success forKey:kIsSuccessArchiveKey]; [encoder encodeObject:self.detail forKey:kDetailArchiveKey]; } - (instancetype)initWithActivityType:(FIRIAMActivityType)type isSuccessful:(BOOL)isSuccessful withDetail:(NSString *)detail timestamp:(nullable NSDate *)timestamp { if (self = [super init]) { _activityType = type; _success = isSuccessful; _detail = detail; _timestamp = timestamp ? timestamp : [[NSDate alloc] init]; } return self; } - (NSString *)displayStringForActivityType { switch (self.activityType) { case FIRIAMActivityTypeFetchMessage: return @"Message Fetching"; case FIRIAMActivityTypeRenderMessage: return @"Message Rendering"; case FIRIAMActivityTypeDismissMessage: return @"Message Dismiss"; case FIRIAMActivityTypeCheckForOnOpenMessage: return @"OnOpen Msg Check"; case FIRIAMActivityTypeCheckForAnalyticsEventMessage: return @"Analytic Msg Check"; case FIRIAMActivityTypeCheckForFetch: return @"Fetch Check"; } } @end @interface FIRIAMActivityLogger () @property(nonatomic) BOOL isDirty; // always insert at the head of this array so that they are always in anti-chronological order @property(nonatomic, nonnull) NSMutableArray *activityRecords; // When we see the number of log records goes beyond maxRecordCountBeforeReduce, we would trigger // a reduction action which would bring the array length to be the size as defined by // newSizeAfterReduce @property(nonatomic, readonly) NSInteger maxRecordCountBeforeReduce; @property(nonatomic, readonly) NSInteger newSizeAfterReduce; @end @implementation FIRIAMActivityLogger - (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce withSizeAfterReduce:(NSInteger)sizeAfterReduce verboseMode:(BOOL)verboseMode loadFromCache:(BOOL)loadFromCache { if (self = [super init]) { _maxRecordCountBeforeReduce = maxBeforeReduce; _newSizeAfterReduce = sizeAfterReduce; _activityRecords = [[NSMutableArray alloc] init]; _verboseMode = verboseMode; _isDirty = NO; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillBecomeInactive:) name:UIApplicationWillResignActiveNotification object:nil]; if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillBecomeInactive:) name:UISceneWillDeactivateNotification object:nil]; } if (loadFromCache) { @try { [self loadFromCachePath:nil]; } @catch (NSException *exception) { FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310003", @"Non-fatal exception in loading persisted activity log records: %@.", exception); } } } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } + (NSString *)determineCacheFilePath { static NSString *logCachePath; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *cacheDirPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; logCachePath = [NSString stringWithFormat:@"%@/firebase-iam-activity-log-cache", cacheDirPath]; FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310001", @"Persistent file path for activity log data is %@", logCachePath); }); return logCachePath; } - (void)loadFromCachePath:(NSString *)cacheFilePath { NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; id fetchedActivityRecords; NSData *data = [NSData dataWithContentsOfFile:filePath]; if (data) { fetchedActivityRecords = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[FIRIAMActivityRecord class], [NSMutableArray class], nil] fromData:data error:nil]; } if (fetchedActivityRecords) { @synchronized(self) { self.activityRecords = (NSMutableArray *)fetchedActivityRecords; self.isDirty = NO; } } } - (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath { NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; @synchronized(self) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" BOOL result = [NSKeyedArchiver archiveRootObject:self.activityRecords toFile:filePath]; #pragma clang diagnostic pop if (result) { self.isDirty = NO; } return result; } } - (void)appWillBecomeInactive:(NSNotification *)notification { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310004", @"App will become inactive, save" " activity logs"); if (self.isDirty) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ if ([self saveIntoCacheWithPath:nil]) { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310002", @"Persisting activity log data is was successful"); } else { FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310005", @"Persisting activity log data has failed"); } }); } } // Helper function to determine if a given activity type should be recorded under // non verbose type. + (BOOL)isMandatoryType:(FIRIAMActivityType)type { switch (type) { case FIRIAMActivityTypeFetchMessage: case FIRIAMActivityTypeRenderMessage: case FIRIAMActivityTypeDismissMessage: return YES; default: return NO; } } - (void)addLogRecord:(FIRIAMActivityRecord *)newRecord { if (self.verboseMode || [FIRIAMActivityLogger isMandatoryType:newRecord.activityType]) { @synchronized(self) { [self.activityRecords insertObject:newRecord atIndex:0]; if (self.activityRecords.count >= self.maxRecordCountBeforeReduce) { NSRange removeRange; removeRange.location = self.newSizeAfterReduce; removeRange.length = self.maxRecordCountBeforeReduce - self.newSizeAfterReduce; [self.activityRecords removeObjectsInRange:removeRange]; } self.isDirty = YES; } } } - (NSArray *)readRecords { return [self.activityRecords copy]; } @end #endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION