/* * Copyright 2019 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 "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseABTesting/Sources/Private/FirebaseABTestingInternal.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time"; static NSString *const kServiceOrigin = @"frc"; static NSString *const kMethodNameLatestStartTime = @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:"; @interface RCNConfigExperiment () @property(nonatomic, strong) NSMutableArray *experimentPayloads; ///< Experiment payloads. @property(nonatomic, strong) NSMutableDictionary *experimentMetadata; ///< Experiment metadata @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. @property(nonatomic, strong) FIRExperimentController *experimentController; @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; @end @implementation RCNConfigExperiment /// Designated initializer - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager experimentController:(FIRExperimentController *)controller { self = [super init]; if (self) { _experimentPayloads = [[NSMutableArray alloc] init]; _experimentMetadata = [[NSMutableDictionary alloc] init]; _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init]; [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; // Locale needs to be hardcoded. See // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details. [_experimentStartTimeDateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; _DBManager = DBManager; _experimentController = controller; [self loadExperimentFromTable]; } return self; } - (void)loadExperimentFromTable { if (!_DBManager) { return; } __weak RCNConfigExperiment *weakSelf = self; RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary *result) { RCNConfigExperiment *strongSelf = weakSelf; if (strongSelf == nil) { return; } if (result[@RCNExperimentTableKeyPayload]) { [strongSelf->_experimentPayloads removeAllObjects]; for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) { NSError *error; id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment options:kNilOptions error:&error]; if (!experimentPayloadJSON || error) { FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031", @"Experiment payload could not be parsed as JSON."); } else { [strongSelf->_experimentPayloads addObject:experiment]; } } } if (result[@RCNExperimentTableKeyMetadata]) { strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy]; } }; [_DBManager loadExperimentWithCompletionHandler:completionHandler]; } - (void)updateExperimentsWithResponse:(NSArray *> *)response { // cache fetched experiment payloads. [_experimentPayloads removeAllObjects]; [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload]; for (NSDictionary *experiment in response) { NSError *error; NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment options:kNilOptions error:&error]; if (!JSONPayload || error) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030", @"Invalid experiment payload to be serialized."); } else { [_experimentPayloads addObject:JSONPayload]; [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload value:JSONPayload completionHandler:nil]; } } } - (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler { FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init]; // Get the last experiment start time prior to the latest payload. NSTimeInterval lastStartTime = [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue]; // Update the last experiment start time with the latest payload. [self updateExperimentStartTime]; [self.experimentController updateExperimentsWithServiceOrigin:kServiceOrigin events:lifecycleEvent policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest lastStartTime:lastStartTime payloads:_experimentPayloads completionHandler:handler]; } - (void)updateExperimentStartTime { NSTimeInterval existingLastStartTime = [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue]; NSTimeInterval latestStartTime = [self latestStartTimeWithExistingLastStartTime:existingLastStartTime]; _experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime); if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028", @"Invalid fetched experiment metadata to be serialized."); return; } NSError *error; NSData *serializedExperimentMetadata = [NSJSONSerialization dataWithJSONObject:_experimentMetadata options:NSJSONWritingPrettyPrinted error:&error]; [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata value:serializedExperimentMetadata completionHandler:nil]; } - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime { return [self.experimentController latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime andPayloads:_experimentPayloads]; } @end