RCNConfigSettings.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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/RCNConfigSettings.h"
  17. #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
  18. #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
  19. #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
  20. #import "FirebaseRemoteConfig/Sources/RCNDevice.h"
  21. #import "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h"
  22. #import <FirebaseCore/FIRApp.h>
  23. #import <FirebaseCore/FIRLogger.h>
  24. #import <FirebaseCore/FIROptions.h>
  25. #import <GoogleUtilities/GULAppEnvironmentUtil.h>
  26. static NSString *const kRCNGroupPrefix = @"frc.group.";
  27. static NSString *const kRCNUserDefaultsKeyNamelastETag = @"lastETag";
  28. static NSString *const kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = @"lastSuccessfulFetchTime";
  29. static const int kRCNExponentialBackoffMinimumInterval = 60 * 2; // 2 mins.
  30. static const int kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4; // 4 hours.
  31. @interface RCNConfigSettings () {
  32. /// A list of successful fetch timestamps in seconds.
  33. NSMutableArray *_successFetchTimes;
  34. /// A list of failed fetch timestamps in seconds.
  35. NSMutableArray *_failureFetchTimes;
  36. /// Device conditions since last successful fetch from the backend. Device conditions including
  37. /// app
  38. /// version, iOS version, device localte, language, GMP project ID and Game project ID. Used for
  39. /// determing whether to throttle.
  40. NSMutableDictionary *_deviceContext;
  41. /// Custom variables (aka App context digest). This is the pending custom variables request before
  42. /// fetching.
  43. NSMutableDictionary *_customVariables;
  44. /// Cached internal metadata from internal metadata table. It contains customized information such
  45. /// as HTTP connection timeout, HTTP read timeout, success/failure throttling rate and time
  46. /// interval. Client has the default value of each parameters, they are only saved in
  47. /// internalMetadata if they have been customize by developers.
  48. NSMutableDictionary *_internalMetadata;
  49. /// Last fetch status.
  50. FIRRemoteConfigFetchStatus _lastFetchStatus;
  51. /// Last fetch Error.
  52. FIRRemoteConfigError _lastFetchError;
  53. /// The time of last apply timestamp.
  54. NSTimeInterval _lastApplyTimeInterval;
  55. /// The time of last setDefaults timestamp.
  56. NSTimeInterval _lastSetDefaultsTimeInterval;
  57. /// The database manager.
  58. RCNConfigDBManager *_DBManager;
  59. // The namespace for this instance.
  60. NSString *_FIRNamespace;
  61. // The Google App ID of the configured FIRApp.
  62. NSString *_googleAppID;
  63. /// The user defaults manager scoped to this RC instance of FIRApp and namespace.
  64. RCNUserDefaultsManager *_userDefaultsManager;
  65. }
  66. @end
  67. @implementation RCNConfigSettings
  68. - (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager
  69. namespace:(NSString *)FIRNamespace
  70. firebaseAppName:(NSString *)appName
  71. googleAppID:(NSString *)googleAppID {
  72. self = [super init];
  73. if (self) {
  74. _FIRNamespace = FIRNamespace;
  75. _googleAppID = googleAppID;
  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. _minimumFetchInterval = RCNDefaultMinimumFetchInterval;
  83. _deviceContext = [[NSMutableDictionary alloc] init];
  84. _customVariables = [[NSMutableDictionary alloc] init];
  85. _successFetchTimes = [[NSMutableArray alloc] init];
  86. _failureFetchTimes = [[NSMutableArray alloc] init];
  87. _DBManager = manager;
  88. _internalMetadata = [[_DBManager loadInternalMetadataTable] mutableCopy];
  89. if (!_internalMetadata) {
  90. _internalMetadata = [[NSMutableDictionary alloc] init];
  91. }
  92. _userDefaultsManager = [[RCNUserDefaultsManager alloc] initWithAppName:appName
  93. bundleID:_bundleIdentifier
  94. namespace:_FIRNamespace];
  95. _isFetchInProgress = NO;
  96. }
  97. return self;
  98. }
  99. #pragma mark - read from / update userDefaults
  100. - (NSString *)lastETag {
  101. return [_userDefaultsManager lastETag];
  102. }
  103. - (void)setLastETag:(NSString *)lastETag {
  104. [_userDefaultsManager setLastETag:lastETag];
  105. }
  106. - (NSTimeInterval)lastFetchTimeInterval {
  107. return _userDefaultsManager.lastFetchTime;
  108. }
  109. // TODO: Update logic for app extensions as required.
  110. - (void)updateLastFetchTimeInterval:(NSTimeInterval)lastFetchTimeInterval {
  111. _userDefaultsManager.lastFetchTime = lastFetchTimeInterval;
  112. }
  113. #pragma mark - load from DB
  114. - (NSDictionary *)loadConfigFromMetadataTable {
  115. NSDictionary *metadata = [[_DBManager loadMetadataWithBundleIdentifier:_bundleIdentifier] copy];
  116. if (metadata) {
  117. // TODO: Remove (all metadata in general) once ready to
  118. // migrate to user defaults completely.
  119. if (metadata[RCNKeyDeviceContext]) {
  120. self->_deviceContext = [metadata[RCNKeyDeviceContext] mutableCopy];
  121. }
  122. if (metadata[RCNKeyAppContext]) {
  123. self->_customVariables = [metadata[RCNKeyAppContext] mutableCopy];
  124. }
  125. if (metadata[RCNKeySuccessFetchTime]) {
  126. self->_successFetchTimes = [metadata[RCNKeySuccessFetchTime] mutableCopy];
  127. }
  128. if (metadata[RCNKeyFailureFetchTime]) {
  129. self->_failureFetchTimes = [metadata[RCNKeyFailureFetchTime] mutableCopy];
  130. }
  131. if (metadata[RCNKeyLastFetchStatus]) {
  132. self->_lastFetchStatus =
  133. (FIRRemoteConfigFetchStatus)[metadata[RCNKeyLastFetchStatus] intValue];
  134. }
  135. if (metadata[RCNKeyLastFetchError]) {
  136. self->_lastFetchError = (FIRRemoteConfigError)[metadata[RCNKeyLastFetchError] intValue];
  137. }
  138. if (metadata[RCNKeyLastApplyTime]) {
  139. self->_lastApplyTimeInterval = [metadata[RCNKeyLastApplyTime] doubleValue];
  140. }
  141. if (metadata[RCNKeyLastFetchStatus]) {
  142. self->_lastSetDefaultsTimeInterval = [metadata[RCNKeyLastSetDefaultsTime] doubleValue];
  143. }
  144. }
  145. return metadata;
  146. }
  147. #pragma mark - update DB/cached
  148. // Update internal metadata content to cache and DB.
  149. - (void)updateInternalContentWithResponse:(NSDictionary *)response {
  150. // Remove all the keys with current pakcage name.
  151. [_DBManager deleteRecordWithBundleIdentifier:_bundleIdentifier isInternalDB:YES];
  152. for (NSString *key in _internalMetadata.allKeys) {
  153. if ([key hasPrefix:_bundleIdentifier]) {
  154. [_internalMetadata removeObjectForKey:key];
  155. }
  156. }
  157. for (NSString *entry in response) {
  158. NSData *val = [response[entry] dataUsingEncoding:NSUTF8StringEncoding];
  159. NSArray *values = @[ entry, val ];
  160. _internalMetadata[entry] = response[entry];
  161. [self updateInternalMetadataTableWithValues:values];
  162. }
  163. }
  164. - (void)updateInternalMetadataTableWithValues:(NSArray *)values {
  165. [_DBManager insertInternalMetadataTableWithValues:values completionHandler:nil];
  166. }
  167. /// If the last fetch was not successful, update the (exponential backoff) period that we wait until
  168. /// fetching again. Any subsequent fetch requests will be checked and allowed only if past this
  169. /// throttle end time.
  170. - (void)updateExponentialBackoffTime {
  171. // If not in exponential backoff mode, reset the retry interval.
  172. if (_lastFetchStatus == FIRRemoteConfigFetchStatusSuccess) {
  173. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057",
  174. @"Throttling: Entering exponential backoff mode.");
  175. _exponentialBackoffRetryInterval = kRCNExponentialBackoffMinimumInterval;
  176. } else {
  177. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057",
  178. @"Throttling: Updating throttling interval.");
  179. // Double the retry interval until we hit the truncated exponential backoff. More info here:
  180. // https://cloud.google.com/storage/docs/exponential-backoff
  181. _exponentialBackoffRetryInterval =
  182. ((_exponentialBackoffRetryInterval * 2) < kRCNExponentialBackoffMaximumInterval)
  183. ? _exponentialBackoffRetryInterval * 2
  184. : _exponentialBackoffRetryInterval;
  185. }
  186. // Randomize the next retry interval.
  187. int randomPlusMinusInterval = ((arc4random() % 2) == 0) ? -1 : 1;
  188. NSTimeInterval randomizedRetryInterval =
  189. _exponentialBackoffRetryInterval +
  190. (0.5 * _exponentialBackoffRetryInterval * randomPlusMinusInterval);
  191. _exponentialBackoffThrottleEndTime =
  192. [[NSDate date] timeIntervalSince1970] + randomizedRetryInterval;
  193. }
  194. - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess {
  195. FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000056", @"Updating metadata with fetch result.");
  196. if (!fetchSuccess) {
  197. [self updateExponentialBackoffTime];
  198. }
  199. [self updateFetchTimeWithSuccessFetch:fetchSuccess];
  200. _lastFetchStatus =
  201. fetchSuccess ? FIRRemoteConfigFetchStatusSuccess : FIRRemoteConfigFetchStatusFailure;
  202. _lastFetchError = fetchSuccess ? FIRRemoteConfigErrorUnknown : FIRRemoteConfigErrorInternalError;
  203. if (fetchSuccess) {
  204. [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]];
  205. // Note: We expect the googleAppID to always be available.
  206. _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID);
  207. }
  208. [self updateMetadataTable];
  209. }
  210. - (void)updateFetchTimeWithSuccessFetch:(BOOL)isSuccessfulFetch {
  211. NSTimeInterval epochTimeInterval = [[NSDate date] timeIntervalSince1970];
  212. if (isSuccessfulFetch) {
  213. [_successFetchTimes addObject:@(epochTimeInterval)];
  214. } else {
  215. [_failureFetchTimes addObject:@(epochTimeInterval)];
  216. }
  217. }
  218. - (void)updateMetadataTable {
  219. [_DBManager deleteRecordWithBundleIdentifier:_bundleIdentifier isInternalDB:NO];
  220. NSError *error;
  221. // Objects to be serialized cannot be invalid.
  222. if (!_bundleIdentifier) {
  223. return;
  224. }
  225. if (![NSJSONSerialization isValidJSONObject:_customVariables]) {
  226. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
  227. @"Invalid custom variables to be serialized.");
  228. return;
  229. }
  230. if (![NSJSONSerialization isValidJSONObject:_deviceContext]) {
  231. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000029",
  232. @"Invalid device context to be serialized.");
  233. return;
  234. }
  235. if (![NSJSONSerialization isValidJSONObject:_successFetchTimes]) {
  236. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000031",
  237. @"Invalid success fetch times to be serialized.");
  238. return;
  239. }
  240. if (![NSJSONSerialization isValidJSONObject:_failureFetchTimes]) {
  241. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000032",
  242. @"Invalid failure fetch times to be serialized.");
  243. return;
  244. }
  245. NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:_customVariables
  246. options:NSJSONWritingPrettyPrinted
  247. error:&error];
  248. NSData *serializedDeviceContext =
  249. [NSJSONSerialization dataWithJSONObject:_deviceContext
  250. options:NSJSONWritingPrettyPrinted
  251. error:&error];
  252. // The digestPerNamespace is not used and only meant for backwards DB compatibility.
  253. NSData *serializedDigestPerNamespace =
  254. [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
  255. NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:_successFetchTimes
  256. options:NSJSONWritingPrettyPrinted
  257. error:&error];
  258. NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:_failureFetchTimes
  259. options:NSJSONWritingPrettyPrinted
  260. error:&error];
  261. if (!serializedDigestPerNamespace || !serializedDeviceContext || !serializedAppContext ||
  262. !serializedSuccessTime || !serializedFailureTime) {
  263. return;
  264. }
  265. NSDictionary *columnNameToValue = @{
  266. RCNKeyBundleIdentifier : _bundleIdentifier,
  267. RCNKeyFetchTime : @(self.lastFetchTimeInterval),
  268. RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
  269. RCNKeyDeviceContext : serializedDeviceContext,
  270. RCNKeyAppContext : serializedAppContext,
  271. RCNKeySuccessFetchTime : serializedSuccessTime,
  272. RCNKeyFailureFetchTime : serializedFailureTime,
  273. RCNKeyLastFetchStatus : [NSString stringWithFormat:@"%ld", (long)_lastFetchStatus],
  274. RCNKeyLastFetchError : [NSString stringWithFormat:@"%ld", (long)_lastFetchError],
  275. RCNKeyLastApplyTime : @(_lastApplyTimeInterval),
  276. RCNKeyLastSetDefaultsTime : @(_lastSetDefaultsTimeInterval)
  277. };
  278. [_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:nil];
  279. }
  280. #pragma mark - fetch request
  281. /// Returns a fetch request with the latest device and config change.
  282. /// Whenever user issues a fetch api call, collect the latest request.
  283. - (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties {
  284. // Note: We only set user properties as mentioned in the new REST API Design doc
  285. NSString *ret = [NSString stringWithFormat:@"{"];
  286. ret = [ret stringByAppendingString:[NSString stringWithFormat:@"app_instance_id:'%@'",
  287. _configInstanceID]];
  288. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_instance_id_token:'%@'",
  289. _configInstanceIDToken]];
  290. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_id:'%@'", _googleAppID]];
  291. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", country_code:'%@'",
  292. FIRRemoteConfigDeviceCountry()]];
  293. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", language_code:'%@'",
  294. FIRRemoteConfigDeviceLocale()]];
  295. ret = [ret
  296. stringByAppendingString:[NSString stringWithFormat:@", platform_version:'%@'",
  297. [GULAppEnvironmentUtil systemVersion]]];
  298. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", time_zone:'%@'",
  299. FIRRemoteConfigTimezone()]];
  300. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", package_name:'%@'",
  301. _bundleIdentifier]];
  302. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_version:'%@'",
  303. FIRRemoteConfigAppVersion()]];
  304. ret = [ret stringByAppendingString:[NSString stringWithFormat:@", sdk_version:'%d'",
  305. FIRRemoteConfigSDKVersion()]];
  306. if (userProperties && userProperties.count > 0) {
  307. NSError *error;
  308. NSData *jsonData = [NSJSONSerialization dataWithJSONObject:userProperties
  309. options:0
  310. error:&error];
  311. if (!error) {
  312. ret = [ret
  313. stringByAppendingString:[NSString
  314. stringWithFormat:@", analytics_user_properties:%@",
  315. [[NSString alloc]
  316. initWithData:jsonData
  317. encoding:NSUTF8StringEncoding]]];
  318. }
  319. }
  320. ret = [ret stringByAppendingString:@"}"];
  321. return ret;
  322. }
  323. #pragma mark - getter/setter
  324. - (void)setLastFetchError:(FIRRemoteConfigError)lastFetchError {
  325. if (_lastFetchError != lastFetchError) {
  326. _lastFetchError = lastFetchError;
  327. [_DBManager updateMetadataWithOption:RCNUpdateOptionFetchStatus
  328. values:@[ @(_lastFetchStatus), @(_lastFetchError) ]
  329. completionHandler:nil];
  330. }
  331. }
  332. - (NSArray *)successFetchTimes {
  333. return [_successFetchTimes copy];
  334. }
  335. - (NSArray *)failureFetchTimes {
  336. return [_failureFetchTimes copy];
  337. }
  338. - (NSDictionary *)customVariables {
  339. return [_customVariables copy];
  340. }
  341. - (NSDictionary *)internalMetadata {
  342. return [_internalMetadata copy];
  343. }
  344. - (NSDictionary *)deviceContext {
  345. return [_deviceContext copy];
  346. }
  347. - (void)setCustomVariables:(NSDictionary *)customVariables {
  348. _customVariables = [[NSMutableDictionary alloc] initWithDictionary:customVariables];
  349. [self updateMetadataTable];
  350. }
  351. - (void)setMinimumFetchInterval:(NSTimeInterval)minimumFetchInterval {
  352. if (minimumFetchInterval < 0) {
  353. _minimumFetchInterval = 0;
  354. } else {
  355. _minimumFetchInterval = minimumFetchInterval;
  356. }
  357. }
  358. - (void)setFetchTimeout:(NSTimeInterval)fetchTimeout {
  359. if (fetchTimeout <= 0) {
  360. _fetchTimeout = RCNHTTPDefaultConnectionTimeout;
  361. } else {
  362. _fetchTimeout = fetchTimeout;
  363. }
  364. }
  365. - (void)setLastApplyTimeInterval:(NSTimeInterval)lastApplyTimestamp {
  366. _lastApplyTimeInterval = lastApplyTimestamp;
  367. [_DBManager updateMetadataWithOption:RCNUpdateOptionApplyTime
  368. values:@[ @(lastApplyTimestamp) ]
  369. completionHandler:nil];
  370. }
  371. - (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp {
  372. _lastSetDefaultsTimeInterval = lastSetDefaultsTimestamp;
  373. [_DBManager updateMetadataWithOption:RCNUpdateOptionDefaultTime
  374. values:@[ @(lastSetDefaultsTimestamp) ]
  375. completionHandler:nil];
  376. }
  377. #pragma mark Throttling
  378. - (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval {
  379. if (self.lastFetchTimeInterval == 0) return YES;
  380. // Check if last config fetch is within minimum fetch interval in seconds.
  381. NSTimeInterval diffInSeconds = [[NSDate date] timeIntervalSince1970] - self.lastFetchTimeInterval;
  382. return diffInSeconds > minimumFetchInterval;
  383. }
  384. - (BOOL)shouldThrottle {
  385. NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
  386. return ((self.lastFetchTimeInterval > 0) &&
  387. (_lastFetchStatus != FIRRemoteConfigFetchStatusSuccess) &&
  388. (_exponentialBackoffThrottleEndTime - now > 0));
  389. }
  390. @end