RCNConfigContent.m 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
  17. #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
  18. #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
  19. #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
  20. #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
  21. #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
  22. #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
  23. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  24. static NSString *const kAffectedParameterKeys = @"affectedParameterKeys";
  25. @implementation RCNConfigContent {
  26. /// Active config data that is currently used.
  27. NSMutableDictionary *_activeConfig;
  28. /// Pending config (aka Fetched config) data that is latest data from server that might or might
  29. /// not be applied.
  30. NSMutableDictionary *_fetchedConfig;
  31. /// Default config provided by user.
  32. NSMutableDictionary *_defaultConfig;
  33. /// Active Personalization metadata that is currently used.
  34. NSDictionary *_activePersonalization;
  35. /// Pending Personalization metadata that is latest data from server that might or might not be
  36. /// applied.
  37. NSDictionary *_fetchedPersonalization;
  38. /// DBManager
  39. RCNConfigDBManager *_DBManager;
  40. /// Current bundle identifier;
  41. NSString *_bundleIdentifier;
  42. /// Blocks all config reads until we have read from the database. This only
  43. /// potentially blocks on the first read. Should be a no-wait for all subsequent reads once we
  44. /// have data read into memory from the database.
  45. dispatch_group_t _dispatch_group;
  46. /// Boolean indicating if initial DB load of fetched,active and default config has succeeded.
  47. BOOL _isConfigLoadFromDBCompleted;
  48. /// Boolean indicating that the load from database has initiated at least once.
  49. BOOL _isDatabaseLoadAlreadyInitiated;
  50. }
  51. /// Default timeout when waiting to read data from database.
  52. const NSTimeInterval kDatabaseLoadTimeoutSecs = 30.0;
  53. /// Singleton instance of RCNConfigContent.
  54. + (instancetype)sharedInstance {
  55. static dispatch_once_t onceToken;
  56. static RCNConfigContent *sharedInstance;
  57. dispatch_once(&onceToken, ^{
  58. sharedInstance =
  59. [[RCNConfigContent alloc] initWithDBManager:[RCNConfigDBManager sharedInstance]];
  60. });
  61. return sharedInstance;
  62. }
  63. - (instancetype)init {
  64. NSAssert(NO, @"Invalid initializer.");
  65. return nil;
  66. }
  67. /// Designated initializer
  68. - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager {
  69. self = [super init];
  70. if (self) {
  71. _activeConfig = [[NSMutableDictionary alloc] init];
  72. _fetchedConfig = [[NSMutableDictionary alloc] init];
  73. _defaultConfig = [[NSMutableDictionary alloc] init];
  74. _activePersonalization = [[NSDictionary alloc] init];
  75. _fetchedPersonalization = [[NSDictionary alloc] init];
  76. _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
  77. if (!_bundleIdentifier) {
  78. FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038",
  79. @"Main bundle identifier is missing. Remote Config might not work properly.");
  80. _bundleIdentifier = @"";
  81. }
  82. _DBManager = DBManager;
  83. // Waits for both config and Personalization data to load.
  84. _dispatch_group = dispatch_group_create();
  85. [self loadConfigFromMainTable];
  86. }
  87. return self;
  88. }
  89. // Blocking call that returns true/false once database load completes / times out.
  90. // @return Initialization status.
  91. - (BOOL)initializationSuccessful {
  92. RCN_MUST_NOT_BE_MAIN_THREAD();
  93. BOOL isDatabaseLoadSuccessful = [self checkAndWaitForInitialDatabaseLoad];
  94. return isDatabaseLoadSuccessful;
  95. }
  96. #pragma mark - database
  97. /// This method is only meant to be called at init time. The underlying logic will need to be
  98. /// revaluated if the assumption changes at a later time.
  99. - (void)loadConfigFromMainTable {
  100. if (!_DBManager) {
  101. return;
  102. }
  103. NSAssert(!_isDatabaseLoadAlreadyInitiated, @"Database load has already been initiated");
  104. _isDatabaseLoadAlreadyInitiated = true;
  105. dispatch_group_enter(_dispatch_group);
  106. [_DBManager
  107. loadMainWithBundleIdentifier:_bundleIdentifier
  108. completionHandler:^(BOOL success, NSDictionary *fetchedConfig,
  109. NSDictionary *activeConfig, NSDictionary *defaultConfig) {
  110. self->_fetchedConfig = [fetchedConfig mutableCopy];
  111. self->_activeConfig = [activeConfig mutableCopy];
  112. self->_defaultConfig = [defaultConfig mutableCopy];
  113. dispatch_group_leave(self->_dispatch_group);
  114. }];
  115. // TODO(karenzeng): Refactor personalization to be returned in loadMainWithBundleIdentifier above
  116. dispatch_group_enter(_dispatch_group);
  117. [_DBManager loadPersonalizationWithCompletionHandler:^(
  118. BOOL success, NSDictionary *fetchedPersonalization,
  119. NSDictionary *activePersonalization, NSDictionary *defaultConfig) {
  120. self->_fetchedPersonalization = [fetchedPersonalization copy];
  121. self->_activePersonalization = [activePersonalization copy];
  122. dispatch_group_leave(self->_dispatch_group);
  123. }];
  124. }
  125. /// Update the current config result to main table.
  126. /// @param values Values in a row to write to the table.
  127. /// @param source The source the config data is coming from. It determines which table to write to.
  128. - (void)updateMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source {
  129. [_DBManager insertMainTableWithValues:values fromSource:source completionHandler:nil];
  130. }
  131. #pragma mark - update
  132. /// This function is for copying dictionary when user set up a default config or when user clicks
  133. /// activate. For now the DBSource can only be Active or Default.
  134. - (void)copyFromDictionary:(NSDictionary *)fromDict
  135. toSource:(RCNDBSource)DBSource
  136. forNamespace:(NSString *)FIRNamespace {
  137. // Make sure database load has completed.
  138. [self checkAndWaitForInitialDatabaseLoad];
  139. NSMutableDictionary *toDict;
  140. if (!fromDict) {
  141. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000007",
  142. @"The source dictionary to copy from does not exist.");
  143. return;
  144. }
  145. FIRRemoteConfigSource source = FIRRemoteConfigSourceRemote;
  146. switch (DBSource) {
  147. case RCNDBSourceDefault:
  148. toDict = _defaultConfig;
  149. source = FIRRemoteConfigSourceDefault;
  150. break;
  151. case RCNDBSourceFetched:
  152. FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000008",
  153. @"This shouldn't happen. Destination dictionary should never be pending type.");
  154. return;
  155. case RCNDBSourceActive:
  156. toDict = _activeConfig;
  157. source = FIRRemoteConfigSourceRemote;
  158. [toDict removeObjectForKey:FIRNamespace];
  159. break;
  160. default:
  161. toDict = _activeConfig;
  162. source = FIRRemoteConfigSourceRemote;
  163. [toDict removeObjectForKey:FIRNamespace];
  164. break;
  165. }
  166. // Completely wipe out DB first.
  167. [_DBManager deleteRecordFromMainTableWithNamespace:FIRNamespace
  168. bundleIdentifier:_bundleIdentifier
  169. fromSource:DBSource];
  170. toDict[FIRNamespace] = [[NSMutableDictionary alloc] init];
  171. NSDictionary *config = fromDict[FIRNamespace];
  172. for (NSString *key in config) {
  173. if (DBSource == FIRRemoteConfigSourceDefault) {
  174. NSObject *value = config[key];
  175. NSData *valueData;
  176. if ([value isKindOfClass:[NSData class]]) {
  177. valueData = (NSData *)value;
  178. } else if ([value isKindOfClass:[NSString class]]) {
  179. valueData = [(NSString *)value dataUsingEncoding:NSUTF8StringEncoding];
  180. } else if ([value isKindOfClass:[NSNumber class]]) {
  181. NSString *strValue = [(NSNumber *)value stringValue];
  182. valueData = [(NSString *)strValue dataUsingEncoding:NSUTF8StringEncoding];
  183. } else if ([value isKindOfClass:[NSDate class]]) {
  184. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  185. [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
  186. NSString *strValue = [dateFormatter stringFromDate:(NSDate *)value];
  187. valueData = [(NSString *)strValue dataUsingEncoding:NSUTF8StringEncoding];
  188. } else if ([value isKindOfClass:[NSArray class]]) {
  189. NSError *error;
  190. valueData = [NSJSONSerialization dataWithJSONObject:value options:0 error:&error];
  191. if (error) {
  192. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000076", @"Invalid array value for key '%@'",
  193. key);
  194. }
  195. } else if ([value isKindOfClass:[NSDictionary class]]) {
  196. NSError *error;
  197. valueData = [NSJSONSerialization dataWithJSONObject:value options:0 error:&error];
  198. if (error) {
  199. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000077",
  200. @"Invalid dictionary value for key '%@'", key);
  201. }
  202. } else {
  203. continue;
  204. }
  205. toDict[FIRNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:valueData
  206. source:source];
  207. NSArray *values = @[ _bundleIdentifier, FIRNamespace, key, valueData ];
  208. [self updateMainTableWithValues:values fromSource:DBSource];
  209. } else {
  210. FIRRemoteConfigValue *value = config[key];
  211. toDict[FIRNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:value.dataValue
  212. source:source];
  213. NSArray *values = @[ _bundleIdentifier, FIRNamespace, key, value.dataValue ];
  214. [self updateMainTableWithValues:values fromSource:DBSource];
  215. }
  216. }
  217. }
  218. - (void)updateConfigContentWithResponse:(NSDictionary *)response
  219. forNamespace:(NSString *)currentNamespace {
  220. // Make sure database load has completed.
  221. [self checkAndWaitForInitialDatabaseLoad];
  222. NSString *state = response[RCNFetchResponseKeyState];
  223. if (!state) {
  224. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000049", @"State field in fetch response is nil.");
  225. return;
  226. }
  227. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000059",
  228. @"Updating config content from Response for namespace:%@ with state: %@",
  229. currentNamespace, response[RCNFetchResponseKeyState]);
  230. if ([state isEqualToString:RCNFetchResponseKeyStateNoChange]) {
  231. [self handleNoChangeStateForConfigNamespace:currentNamespace];
  232. return;
  233. }
  234. /// Handle empty config state
  235. if ([state isEqualToString:RCNFetchResponseKeyStateEmptyConfig]) {
  236. [self handleEmptyConfigStateForConfigNamespace:currentNamespace];
  237. return;
  238. }
  239. /// Handle no template state.
  240. if ([state isEqualToString:RCNFetchResponseKeyStateNoTemplate]) {
  241. [self handleNoTemplateStateForConfigNamespace:currentNamespace];
  242. return;
  243. }
  244. /// Handle update state
  245. if ([state isEqualToString:RCNFetchResponseKeyStateUpdate]) {
  246. [self handleUpdateStateForConfigNamespace:currentNamespace
  247. withEntries:response[RCNFetchResponseKeyEntries]];
  248. [self handleUpdatePersonalization:response[RCNFetchResponseKeyPersonalizationMetadata]];
  249. return;
  250. }
  251. }
  252. - (void)activatePersonalization {
  253. _activePersonalization = _fetchedPersonalization;
  254. [_DBManager insertOrUpdatePersonalizationConfig:_activePersonalization
  255. fromSource:RCNDBSourceActive];
  256. }
  257. #pragma mark State handling
  258. - (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace {
  259. if (!_fetchedConfig[currentNamespace]) {
  260. _fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init];
  261. }
  262. }
  263. - (void)handleEmptyConfigStateForConfigNamespace:(NSString *)currentNamespace {
  264. if (_fetchedConfig[currentNamespace]) {
  265. [_fetchedConfig[currentNamespace] removeAllObjects];
  266. } else {
  267. // If namespace has empty status and it doesn't exist in _fetchedConfig, we will
  268. // still add an entry for that namespace. Even if it will not be persisted in database.
  269. // TODO: Add generics for all collection types.
  270. _fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init];
  271. }
  272. [_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace
  273. bundleIdentifier:_bundleIdentifier
  274. fromSource:RCNDBSourceFetched];
  275. }
  276. - (void)handleNoTemplateStateForConfigNamespace:(NSString *)currentNamespace {
  277. // Remove the namespace.
  278. [_fetchedConfig removeObjectForKey:currentNamespace];
  279. [_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace
  280. bundleIdentifier:_bundleIdentifier
  281. fromSource:RCNDBSourceFetched];
  282. }
  283. - (void)handleUpdateStateForConfigNamespace:(NSString *)currentNamespace
  284. withEntries:(NSDictionary *)entries {
  285. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000058", @"Update config in DB for namespace:%@",
  286. currentNamespace);
  287. // Clear before updating
  288. [_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace
  289. bundleIdentifier:_bundleIdentifier
  290. fromSource:RCNDBSourceFetched];
  291. if ([_fetchedConfig objectForKey:currentNamespace]) {
  292. [_fetchedConfig[currentNamespace] removeAllObjects];
  293. } else {
  294. _fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init];
  295. }
  296. // Store the fetched config values.
  297. for (NSString *key in entries) {
  298. NSData *valueData = [entries[key] dataUsingEncoding:NSUTF8StringEncoding];
  299. if (!valueData) {
  300. continue;
  301. }
  302. _fetchedConfig[currentNamespace][key] =
  303. [[FIRRemoteConfigValue alloc] initWithData:valueData source:FIRRemoteConfigSourceRemote];
  304. NSArray *values = @[ _bundleIdentifier, currentNamespace, key, valueData ];
  305. [self updateMainTableWithValues:values fromSource:RCNDBSourceFetched];
  306. }
  307. }
  308. - (void)handleUpdatePersonalization:(NSDictionary *)metadata {
  309. if (!metadata) {
  310. return;
  311. }
  312. _fetchedPersonalization = metadata;
  313. [_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched];
  314. }
  315. #pragma mark - getter/setter
  316. - (NSDictionary *)fetchedConfig {
  317. /// If this is the first time reading the fetchedConfig, we might still be reading it from the
  318. /// database.
  319. [self checkAndWaitForInitialDatabaseLoad];
  320. return _fetchedConfig;
  321. }
  322. - (NSDictionary *)activeConfig {
  323. /// If this is the first time reading the activeConfig, we might still be reading it from the
  324. /// database.
  325. [self checkAndWaitForInitialDatabaseLoad];
  326. return _activeConfig;
  327. }
  328. - (NSDictionary *)defaultConfig {
  329. /// If this is the first time reading the fetchedConfig, we might still be reading it from the
  330. /// database.
  331. [self checkAndWaitForInitialDatabaseLoad];
  332. return _defaultConfig;
  333. }
  334. - (NSDictionary *)activePersonalization {
  335. [self checkAndWaitForInitialDatabaseLoad];
  336. return _activePersonalization;
  337. }
  338. - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace {
  339. /// If this is the first time reading the active metadata, we might still be reading it from the
  340. /// database.
  341. [self checkAndWaitForInitialDatabaseLoad];
  342. return @{
  343. RCNFetchResponseKeyEntries : _activeConfig[FIRNamespace],
  344. RCNFetchResponseKeyPersonalizationMetadata : _activePersonalization
  345. };
  346. }
  347. /// We load the database async at init time. Block all further calls to active/fetched/default
  348. /// configs until load is done.
  349. /// @return Database load completion status.
  350. - (BOOL)checkAndWaitForInitialDatabaseLoad {
  351. /// Wait until load is done. This should be a no-op for subsequent calls.
  352. if (!_isConfigLoadFromDBCompleted) {
  353. intptr_t isErrorOrTimeout = dispatch_group_wait(
  354. _dispatch_group,
  355. dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDatabaseLoadTimeoutSecs * NSEC_PER_SEC)));
  356. if (isErrorOrTimeout) {
  357. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000048",
  358. @"Timed out waiting for fetched config to be loaded from DB");
  359. return false;
  360. }
  361. _isConfigLoadFromDBCompleted = true;
  362. }
  363. return true;
  364. }
  365. /// Load active and fetched experiment payloads and return them in a map.
  366. - (NSDictionary<NSString *, NSMutableArray<NSData *> *> *)loadExperimentsPayloads {
  367. __block NSMutableArray *activeExperimentPayloads = [[NSMutableArray alloc] init];
  368. __block NSMutableArray *experimentPayloads = [[NSMutableArray alloc] init];
  369. /// Load experiments from DB.
  370. RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
  371. if (result[@RCNExperimentTableKeyPayload]) {
  372. experimentPayloads = [result[@RCNExperimentTableKeyPayload] mutableCopy];
  373. }
  374. if (result[@RCNExperimentTableKeyActivePayload]) {
  375. activeExperimentPayloads = [result[@RCNExperimentTableKeyActivePayload] mutableCopy];
  376. }
  377. };
  378. [_DBManager loadExperimentWithCompletionHandler:completionHandler];
  379. return @{
  380. @RCNExperimentTableKeyPayload : experimentPayloads,
  381. @RCNExperimentTableKeyActivePayload : activeExperimentPayloads
  382. };
  383. }
  384. /// Creates a map where the key is the config key and the value if the experiment description.
  385. - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray<NSData *> *)experiments {
  386. NSMutableDictionary<NSString *, NSMutableDictionary *> *experimentsMap =
  387. [[NSMutableDictionary alloc] init];
  388. /// Iterate through all the experiments and check if they contain `affectedParameterKeys`.
  389. for (NSData *experiment in experiments) {
  390. NSError *error;
  391. NSDictionary *experimentJSON =
  392. [NSJSONSerialization JSONObjectWithData:experiment
  393. options:NSJSONReadingMutableContainers
  394. error:&error];
  395. if (!error && experimentJSON) {
  396. if ([experimentJSON objectForKey:kAffectedParameterKeys]) {
  397. NSMutableArray *configKeys =
  398. (NSMutableArray *)[experimentJSON objectForKey:kAffectedParameterKeys];
  399. NSMutableDictionary *experimentCopy = [experimentJSON mutableCopy];
  400. /// Remove `affectedParameterKeys` because the values come out of order and could affect the
  401. /// diffing.
  402. [experimentCopy removeObjectForKey:kAffectedParameterKeys];
  403. /// Map experiments to config keys.
  404. for (NSString *key in configKeys) {
  405. [experimentsMap setObject:experimentCopy forKey:key];
  406. }
  407. }
  408. }
  409. }
  410. return experimentsMap;
  411. }
  412. /// Returns keys that were affected by experiment changes.
  413. - (NSMutableSet<NSString *> *)
  414. getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads
  415. fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads {
  416. NSMutableSet<NSString *> *changedKeys = [[NSMutableSet alloc] init];
  417. /// Create config keys to experiments map.
  418. NSMutableDictionary *activeExperimentsMap = [self createExperimentsMap:activeExperimentPayloads];
  419. NSMutableDictionary *fetchedExperimentsMap = [self createExperimentsMap:experimentPayloads];
  420. /// Iterate through active experiment's keys and compare them to fetched experiment's keys.
  421. for (NSString *key in [activeExperimentsMap allKeys]) {
  422. if (![fetchedExperimentsMap objectForKey:key]) {
  423. [changedKeys addObject:key];
  424. } else {
  425. if (![[activeExperimentsMap objectForKey:key]
  426. isEqualToDictionary:[fetchedExperimentsMap objectForKey:key]]) {
  427. [changedKeys addObject:key];
  428. }
  429. }
  430. }
  431. /// Iterate through fetched experiment's keys and compare them to active experiment's keys.
  432. for (NSString *key in [fetchedExperimentsMap allKeys]) {
  433. if (![activeExperimentsMap objectForKey:key]) {
  434. [changedKeys addObject:key];
  435. } else {
  436. if (![[fetchedExperimentsMap objectForKey:key]
  437. isEqualToDictionary:[activeExperimentsMap objectForKey:key]]) {
  438. [changedKeys addObject:key];
  439. }
  440. }
  441. }
  442. return changedKeys;
  443. }
  444. // Compare fetched config with active config and output what has changed
  445. - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace {
  446. FIRRemoteConfigUpdate *configUpdate;
  447. NSMutableSet<NSString *> *updatedKeys = [[NSMutableSet alloc] init];
  448. NSDictionary *experiments = [self loadExperimentsPayloads];
  449. NSMutableSet *changedExperimentKeys = [self
  450. getKeysAffectedByChangedExperiments:[experiments
  451. objectForKey:@RCNExperimentTableKeyActivePayload]
  452. fetchedExperimentPayloads:[experiments objectForKey:@RCNExperimentTableKeyPayload]];
  453. NSDictionary *fetchedConfig =
  454. _fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init];
  455. NSDictionary *activeConfig =
  456. _activeConfig[FIRNamespace] ? _activeConfig[FIRNamespace] : [[NSDictionary alloc] init];
  457. NSDictionary *fetchedP13n = _fetchedPersonalization;
  458. NSDictionary *activeP13n = _activePersonalization;
  459. // add new/updated params
  460. for (NSString *key in [fetchedConfig allKeys]) {
  461. if (activeConfig[key] == nil ||
  462. ![[activeConfig[key] stringValue] isEqualToString:[fetchedConfig[key] stringValue]]) {
  463. [updatedKeys addObject:key];
  464. }
  465. }
  466. // add deleted params
  467. for (NSString *key in [activeConfig allKeys]) {
  468. if (fetchedConfig[key] == nil) {
  469. [updatedKeys addObject:key];
  470. }
  471. }
  472. // add params with new/updated p13n metadata
  473. for (NSString *key in [fetchedP13n allKeys]) {
  474. if (activeP13n[key] == nil || ![activeP13n[key] isEqualToDictionary:fetchedP13n[key]]) {
  475. [updatedKeys addObject:key];
  476. }
  477. }
  478. // add params with deleted p13n metadata
  479. for (NSString *key in [activeP13n allKeys]) {
  480. if (fetchedP13n[key] == nil) {
  481. [updatedKeys addObject:key];
  482. }
  483. }
  484. // Add params affected by changed experiments.
  485. for (NSString *key in changedExperimentKeys) {
  486. [updatedKeys addObject:key];
  487. }
  488. configUpdate = [[FIRRemoteConfigUpdate alloc] initWithUpdatedKeys:updatedKeys];
  489. return configUpdate;
  490. }
  491. @end