-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathRCNConfigRealtime.m
734 lines (646 loc) · 31.1 KB
/
RCNConfigRealtime.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
/*
* Copyright 2022 Google LLC
*
* 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/RCNConfigRealtime.h"
#import <Foundation/Foundation.h>
#import <GoogleUtilities/GULNSData+zlib.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNDevice.h"
/// URL params
static NSString *const kServerURLDomain = @"https://firebaseremoteconfigrealtime.googleapis.com";
static NSString *const kServerURLVersion = @"/v1";
static NSString *const kServerURLProjects = @"/projects/";
static NSString *const kServerURLNamespaces = @"/namespaces/";
static NSString *const kServerURLQuery = @":streamFetchInvalidations?";
static NSString *const kServerURLKey = @"key=";
/// Realtime API enablement
static NSString *const kServerForbiddenStatusCode = @"\"code\": 403";
/// Header names
static NSString *const kHTTPMethodPost = @"POST"; ///< HTTP request method config fetch using
static NSString *const kContentTypeHeaderName = @"Content-Type"; ///< HTTP Header Field Name
static NSString *const kContentEncodingHeaderName =
@"Content-Encoding"; ///< HTTP Header Field Name
static NSString *const kAcceptEncodingHeaderName = @"Accept"; ///< HTTP Header Field Name
static NSString *const kETagHeaderName = @"etag"; ///< HTTP Header Field Name
static NSString *const kIfNoneMatchETagHeaderName = @"if-none-match"; ///< HTTP Header Field Name
static NSString *const kInstallationsAuthTokenHeaderName = @"x-goog-firebase-installations-auth";
// Sends the bundle ID. Refer to b/130301479 for details.
static NSString *const kiOSBundleIdentifierHeaderName =
@"X-Ios-Bundle-Identifier"; ///< HTTP Header Field Name
/// Retryable HTTP status code.
static NSInteger const kRCNFetchResponseHTTPStatusOk = 200;
static NSInteger const kRCNFetchResponseHTTPStatusClientTimeout = 429;
static NSInteger const kRCNFetchResponseHTTPStatusTooManyRequests = 429;
static NSInteger const kRCNFetchResponseHTTPStatusCodeBadGateway = 502;
static NSInteger const kRCNFetchResponseHTTPStatusCodeServiceUnavailable = 503;
static NSInteger const kRCNFetchResponseHTTPStatusCodeGatewayTimeout = 504;
/// Invalidation message field names.
static NSString *const kTemplateVersionNumberKey = @"latestTemplateVersionNumber";
static NSString *const kIsFeatureDisabled = @"featureDisabled";
static NSTimeInterval gTimeoutSeconds = 330;
static NSInteger const gFetchAttempts = 3;
// Retry parameters
static NSInteger const gMaxRetries = 7;
@interface FIRConfigUpdateListenerRegistration ()
@property(strong, atomic, nonnull) RCNConfigUpdateCompletion completionHandler;
@end
@implementation FIRConfigUpdateListenerRegistration {
RCNConfigRealtime *_realtimeClient;
}
- (instancetype)initWithClient:(RCNConfigRealtime *)realtimeClient
completionHandler:(RCNConfigUpdateCompletion)completionHandler {
self = [super init];
if (self) {
_realtimeClient = realtimeClient;
_completionHandler = completionHandler;
}
return self;
}
- (void)remove {
[self->_realtimeClient removeConfigUpdateListener:_completionHandler];
}
@end
@interface RCNConfigRealtime ()
@property(strong, atomic, nonnull) NSMutableSet<RCNConfigUpdateCompletion> *listeners;
@property(strong, atomic, nonnull) dispatch_queue_t realtimeLockQueue;
@property(strong, atomic, nonnull) NSNotificationCenter *notificationCenter;
@property(strong, atomic) NSURLSession *session;
@property(strong, atomic) NSURLSessionDataTask *dataTask;
@property(strong, atomic) NSMutableURLRequest *request;
@end
@implementation RCNConfigRealtime {
RCNConfigFetch *_configFetch;
RCNConfigSettings *_settings;
FIROptions *_options;
NSString *_namespace;
NSInteger _remainingRetryCount;
bool _isRequestInProgress;
bool _isInBackground;
bool _isRealtimeDisabled;
}
- (instancetype)init:(RCNConfigFetch *)configFetch
settings:(RCNConfigSettings *)settings
namespace:(NSString *)namespace
options:(FIROptions *)options {
self = [super init];
if (self) {
_listeners = [[NSMutableSet alloc] init];
_realtimeLockQueue = [RCNConfigRealtime realtimeRemoteConfigSerialQueue];
_notificationCenter = [NSNotificationCenter defaultCenter];
_configFetch = configFetch;
_settings = settings;
_options = options;
_namespace = namespace;
_remainingRetryCount = MAX(gMaxRetries - [_settings realtimeRetryCount], 1);
_isRequestInProgress = false;
_isRealtimeDisabled = false;
_isInBackground = false;
[self setUpHttpRequest];
[self setUpHttpSession];
[self backgroundChangeListener];
}
return self;
}
/// Singleton instance of serial queue for queuing all incoming RC calls.
+ (dispatch_queue_t)realtimeRemoteConfigSerialQueue {
static dispatch_once_t onceToken;
static dispatch_queue_t realtimeRemoteConfigQueue;
dispatch_once(&onceToken, ^{
realtimeRemoteConfigQueue =
dispatch_queue_create(RCNRemoteConfigQueueLabel, DISPATCH_QUEUE_SERIAL);
});
return realtimeRemoteConfigQueue;
}
- (void)propagateErrors:(NSError *)error {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
for (RCNConfigUpdateCompletion listener in strongSelf->_listeners) {
listener(nil, error);
}
});
}
#pragma mark - Test Only Helpers
// TESTING ONLY
- (void)triggerListenerForTesting:(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate,
NSError *_Nullable error))listener {
listener([[FIRRemoteConfigUpdate alloc] init], nil);
}
#pragma mark - Http Helpers
- (NSString *)constructServerURL {
NSString *serverURLStr = [[NSString alloc] initWithString:kServerURLDomain];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLVersion];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLProjects];
serverURLStr = [serverURLStr stringByAppendingString:_options.GCMSenderID];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLNamespaces];
/// Get the namespace from the fully qualified namespace string of "namespace:FIRAppName".
NSString *namespace = [_namespace substringToIndex:[_namespace rangeOfString:@":"].location];
serverURLStr = [serverURLStr stringByAppendingString:namespace];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLQuery];
if (_options.APIKey) {
serverURLStr = [serverURLStr stringByAppendingString:kServerURLKey];
serverURLStr = [serverURLStr stringByAppendingString:_options.APIKey];
} else {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000071",
@"Missing `APIKey` from `FirebaseOptions`, please ensure the configured "
@"`FirebaseApp` is configured with `FirebaseOptions` that contains an `APIKey`.");
}
return serverURLStr;
}
- (NSString *)FIRAppNameFromFullyQualifiedNamespace {
return [[_namespace componentsSeparatedByString:@":"] lastObject];
}
- (void)reportCompletionOnHandler:(FIRRemoteConfigFetchCompletion)completionHandler
withStatus:(FIRRemoteConfigFetchStatus)status
withError:(NSError *)error {
if (completionHandler) {
dispatch_async(_realtimeLockQueue, ^{
completionHandler(status, error);
});
}
}
/// Refresh installation ID token before fetching config. installation ID is now mandatory for fetch
/// requests to work.(b/14751422).
- (void)refreshInstallationsTokenWithCompletionHandler:
(FIRRemoteConfigFetchCompletion)completionHandler {
FIRInstallations *installations = [FIRInstallations
installationsWithApp:[FIRApp appNamed:[self FIRAppNameFromFullyQualifiedNamespace]]];
if (!installations || !_options.GCMSenderID) {
NSString *errorDescription = @"Failed to get GCMSenderID";
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000074", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
return [self
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}]];
}
__weak RCNConfigRealtime *weakSelf = self;
FIRInstallationsTokenHandler installationsTokenHandler = ^(
FIRInstallationsAuthTokenResult *tokenResult, NSError *error) {
RCNConfigRealtime *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
if (!tokenResult || !tokenResult.authToken || error) {
NSString *errorDescription =
[NSString stringWithFormat:@"Failed to get installations token. Error : %@.", error];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000073", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
return [strongSelf
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}]];
}
/// We have a valid token. Get the backing installationID.
[installations installationIDWithCompletion:^(NSString *_Nullable identifier,
NSError *_Nullable error) {
RCNConfigRealtime *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
// Dispatch to the RC serial queue to update settings on the queue.
dispatch_async(strongSelf->_realtimeLockQueue, ^{
RCNConfigRealtime *strongSelfQueue = weakSelf;
if (strongSelfQueue == nil) {
return;
}
/// Update config settings with the IID and token.
strongSelfQueue->_settings.configInstallationsToken = tokenResult.authToken;
strongSelfQueue->_settings.configInstallationsIdentifier = identifier;
if (!identifier || error) {
NSString *errorDescription =
[NSString stringWithFormat:@"Error getting iid : %@.", error];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000055", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
strongSelfQueue->_settings.isFetchInProgress = NO;
return [strongSelfQueue
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError
errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}]];
}
FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000022", @"Success to get iid : %@.",
strongSelfQueue->_settings.configInstallationsIdentifier);
return [strongSelfQueue reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusNoFetchYet
withError:nil];
});
}];
};
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000039", @"Starting requesting token.");
[installations authTokenWithCompletion:installationsTokenHandler];
}
- (void)createRequestBodyWithCompletion:(void (^)(NSData *_Nonnull requestBody))completion {
__weak __typeof(self) weakSelf = self;
[self refreshInstallationsTokenWithCompletionHandler:^(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) {
__strong __typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
if (![strongSelf->_settings.configInstallationsIdentifier length]) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000013",
@"Installation token retrieval failed. Realtime connection will not include "
@"valid installations token.");
}
[strongSelf.request setValue:strongSelf->_settings.configInstallationsToken
forHTTPHeaderField:kInstallationsAuthTokenHeaderName];
if (strongSelf->_settings.lastETag) {
[strongSelf.request setValue:strongSelf->_settings.lastETag
forHTTPHeaderField:kIfNoneMatchETagHeaderName];
}
NSString *namespace = [strongSelf->_namespace
substringToIndex:[strongSelf->_namespace rangeOfString:@":"].location];
NSString *postBody = [NSString
stringWithFormat:@"{project:'%@', namespace:'%@', lastKnownVersionNumber:'%@', appId:'%@', "
@"sdkVersion:'%@', appInstanceId:'%@'}",
[strongSelf->_options GCMSenderID], namespace,
strongSelf->_configFetch.templateVersionNumber,
strongSelf->_options.googleAppID, FIRRemoteConfigPodVersion(),
strongSelf->_settings.configInstallationsIdentifier];
NSData *postData = [postBody dataUsingEncoding:NSUTF8StringEncoding];
NSError *compressionError;
completion([NSData gul_dataByGzippingData:postData error:&compressionError]);
}];
}
/// Creates request.
- (void)setUpHttpRequest {
NSString *address = [self constructServerURL];
_request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:address]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:gTimeoutSeconds];
[_request setHTTPMethod:kHTTPMethodPost];
[_request setValue:@"application/json" forHTTPHeaderField:kContentTypeHeaderName];
[_request setValue:@"application/json" forHTTPHeaderField:kAcceptEncodingHeaderName];
[_request setValue:@"gzip" forHTTPHeaderField:kContentEncodingHeaderName];
[_request setValue:@"true" forHTTPHeaderField:@"X-Google-GFE-Can-Retry"];
[_request setValue:[_options APIKey] forHTTPHeaderField:@"X-Goog-Api-Key"];
[_request setValue:[[NSBundle mainBundle] bundleIdentifier]
forHTTPHeaderField:kiOSBundleIdentifierHeaderName];
}
/// Makes call to create session.
- (void)setUpHttpSession {
NSURLSessionConfiguration *sessionConfig =
[[NSURLSessionConfiguration defaultSessionConfiguration] copy];
[sessionConfig setTimeoutIntervalForResource:gTimeoutSeconds];
[sessionConfig setTimeoutIntervalForRequest:gTimeoutSeconds];
_session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
}
#pragma mark - Retry Helpers
- (BOOL)canMakeConnection {
BOOL noRunningConnection =
self->_dataTask == nil || self->_dataTask.state != NSURLSessionTaskStateRunning;
BOOL canMakeConnection = noRunningConnection && [self->_listeners count] > 0 &&
!self->_isInBackground && !self->_isRealtimeDisabled;
return canMakeConnection;
}
// Retry mechanism for HTTP connections
- (void)retryHTTPConnection {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
if (!strongSelf || strongSelf->_isInBackground) {
return;
}
if ([strongSelf canMakeConnection] && strongSelf->_remainingRetryCount > 0) {
NSTimeInterval backOffInterval = self->_settings.getRealtimeBackoffInterval;
strongSelf->_remainingRetryCount--;
[strongSelf->_settings setRealtimeRetryCount:[strongSelf->_settings realtimeRetryCount] + 1];
dispatch_time_t executionDelay =
dispatch_time(DISPATCH_TIME_NOW, (backOffInterval * NSEC_PER_SEC));
dispatch_after(executionDelay, strongSelf->_realtimeLockQueue, ^{
[strongSelf beginRealtimeStream];
});
} else {
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorStreamError
userInfo:@{
NSLocalizedDescriptionKey :
@"Unable to connect to the server. Check your connection and try again."
}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000014", @"Cannot establish connection. Error: %@",
error);
[self propagateErrors:error];
}
});
}
- (void)backgroundChangeListener {
[_notificationCenter addObserver:self
selector:@selector(isInForeground)
name:@"UIApplicationWillEnterForegroundNotification"
object:nil];
[_notificationCenter addObserver:self
selector:@selector(isInBackground)
name:@"UIApplicationDidEnterBackgroundNotification"
object:nil];
}
- (void)isInForeground {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
strongSelf->_isInBackground = false;
[strongSelf beginRealtimeStream];
});
}
- (void)isInBackground {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
[strongSelf pauseRealtimeStream];
strongSelf->_isInBackground = true;
});
}
#pragma mark - Autofetch Helpers
- (void)fetchLatestConfig:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
NSInteger attempts = remainingAttempts - 1;
[strongSelf->_configFetch
realtimeFetchConfigWithNoExpirationDuration:gFetchAttempts - attempts
completionHandler:^(FIRRemoteConfigFetchStatus status,
FIRRemoteConfigUpdate *update,
NSError *error) {
if (error != nil) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010",
@"Failed to retrieve config due to fetch error. "
@"Error: %@",
error);
return [self propagateErrors:error];
}
if (status == FIRRemoteConfigFetchStatusSuccess) {
if ([strongSelf->_configFetch.templateVersionNumber
integerValue] >= targetVersion) {
// only notify listeners if there is a change
if ([update updatedKeys].count > 0) {
dispatch_async(strongSelf->_realtimeLockQueue, ^{
for (RCNConfigUpdateCompletion listener in strongSelf
->_listeners) {
listener(update, nil);
}
});
}
} else {
FIRLogDebug(
kFIRLoggerRemoteConfig, @"I-RCN000016",
@"Fetched config's template version is outdated, "
@"re-fetching");
[strongSelf autoFetch:attempts targetVersion:targetVersion];
}
} else {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000016",
@"Fetched config's template version is "
@"outdated, re-fetching");
[strongSelf autoFetch:attempts targetVersion:targetVersion];
}
}];
});
}
- (void)scheduleFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
/// Needs fetch to occur between 0 - 3 seconds. Randomize to not cause DDoS alerts in backend
dispatch_time_t executionDelay =
dispatch_time(DISPATCH_TIME_NOW, arc4random_uniform(4) * NSEC_PER_SEC);
dispatch_after(executionDelay, _realtimeLockQueue, ^{
[self fetchLatestConfig:remainingAttempts targetVersion:targetVersion];
});
}
/// Perform fetch and handle developers callbacks
- (void)autoFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
if (remainingAttempts == 0) {
NSError *error = [NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorNotFetched
userInfo:@{
NSLocalizedDescriptionKey :
@"Unable to fetch the latest version of the template."
}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",
@"Ran out of fetch attempts, cannot find target config version.");
[self propagateErrors:error];
return;
}
[strongSelf scheduleFetch:remainingAttempts targetVersion:targetVersion];
});
}
#pragma mark - NSURLSession Delegates
- (void)evaluateStreamResponse:(NSDictionary *)response error:(NSError *)dataError {
NSInteger updateTemplateVersion = 1;
if (dataError == nil) {
if ([response objectForKey:kTemplateVersionNumberKey]) {
updateTemplateVersion = [[response objectForKey:kTemplateVersionNumberKey] integerValue];
}
if ([response objectForKey:kIsFeatureDisabled]) {
self->_isRealtimeDisabled = [response objectForKey:kIsFeatureDisabled];
}
if (self->_isRealtimeDisabled) {
[self pauseRealtimeStream];
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorUnavailable
userInfo:@{
NSLocalizedDescriptionKey :
@"The server is temporarily unavailable. Try again in a few minutes."
}];
[self propagateErrors:error];
} else {
NSInteger clientTemplateVersion = [_configFetch.templateVersionNumber integerValue];
if (updateTemplateVersion > clientTemplateVersion) {
[self autoFetch:gFetchAttempts targetVersion:updateTemplateVersion];
}
}
} else {
NSError *error =
[NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorMessageInvalid
userInfo:@{NSLocalizedDescriptionKey : @"Unable to parse ConfigUpdate."}];
[self propagateErrors:error];
}
}
/// Delegate to asynchronously handle every new notification that comes over the wire. Auto-fetches
/// and runs callback for each new notification
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
NSError *dataError;
NSString *strData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
/// If response data contains the API enablement link, return the entire message to the user in
/// the form of a error.
if ([strData containsString:kServerForbiddenStatusCode]) {
NSError *error = [NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorStreamError
userInfo:@{NSLocalizedDescriptionKey : strData}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000021", @"Cannot establish connection. %@", error);
[self propagateErrors:error];
return;
}
NSRange endRange = [strData rangeOfString:@"}"];
NSRange beginRange = [strData rangeOfString:@"{"];
if (beginRange.location != NSNotFound && endRange.location != NSNotFound) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000015",
@"Received config update message on stream.");
NSRange msgRange =
NSMakeRange(beginRange.location, endRange.location - beginRange.location + 1);
strData = [strData substringWithRange:msgRange];
data = [strData dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&dataError];
[self evaluateStreamResponse:response error:dataError];
}
}
/// Check if response code is retryable
- (bool)isStatusCodeRetryable:(NSInteger)statusCode {
return statusCode == kRCNFetchResponseHTTPStatusClientTimeout ||
statusCode == kRCNFetchResponseHTTPStatusTooManyRequests ||
statusCode == kRCNFetchResponseHTTPStatusCodeServiceUnavailable ||
statusCode == kRCNFetchResponseHTTPStatusCodeBadGateway ||
statusCode == kRCNFetchResponseHTTPStatusCodeGatewayTimeout;
}
/// Delegate to handle initial reply from the server
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
_isRequestInProgress = false;
NSHTTPURLResponse *_httpURLResponse = (NSHTTPURLResponse *)response;
NSInteger statusCode = [_httpURLResponse statusCode];
if (statusCode == 403) {
completionHandler(NSURLSessionResponseAllow);
return;
}
if (statusCode != kRCNFetchResponseHTTPStatusOk) {
[self->_settings updateRealtimeExponentialBackoffTime];
[self pauseRealtimeStream];
if ([self isStatusCodeRetryable:statusCode]) {
[self retryHTTPConnection];
} else {
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorStreamError
userInfo:@{
NSLocalizedDescriptionKey :
[NSString stringWithFormat:@"Unable to connect to the server. Try again in "
@"a few minutes. Http Status code: %@",
[@(statusCode) stringValue]]
}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000021", @"Cannot establish connection. Error: %@",
error);
}
} else {
/// on success reset retry parameters
_remainingRetryCount = gMaxRetries;
[self->_settings setRealtimeRetryCount:0];
}
completionHandler(NSURLSessionResponseAllow);
}
/// Delegate to handle data task completion
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
_isRequestInProgress = false;
if (error != nil && [error code] != NSURLErrorCancelled) {
[self->_settings updateRealtimeExponentialBackoffTime];
}
[self pauseRealtimeStream];
[self retryHTTPConnection];
}
/// Delegate to handle session invalidation
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
if (!_isRequestInProgress) {
if (error != nil) {
[self->_settings updateRealtimeExponentialBackoffTime];
}
[self pauseRealtimeStream];
[self retryHTTPConnection];
}
}
#pragma mark - Top level methods
- (void)beginRealtimeStream {
__weak __typeof(self) weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf->_settings.getRealtimeBackoffInterval > 0) {
[strongSelf retryHTTPConnection];
return;
}
if ([strongSelf canMakeConnection]) {
__weak __typeof(self) weakSelf = strongSelf;
[strongSelf createRequestBodyWithCompletion:^(NSData *_Nonnull requestBody) {
__strong __typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
strongSelf->_isRequestInProgress = true;
[strongSelf->_request setHTTPBody:requestBody];
strongSelf->_dataTask = [strongSelf->_session dataTaskWithRequest:strongSelf->_request];
[strongSelf->_dataTask resume];
}];
}
});
}
- (void)pauseRealtimeStream {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
if (strongSelf->_dataTask != nil) {
[strongSelf->_dataTask cancel];
strongSelf->_dataTask = nil;
}
});
}
- (FIRConfigUpdateListenerRegistration *)addConfigUpdateListener:
(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate, NSError *_Nullable error))listener {
if (listener == nil) {
return nil;
}
__block id listenerCopy = listener;
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
[strongSelf->_listeners addObject:listenerCopy];
[strongSelf beginRealtimeStream];
});
return [[FIRConfigUpdateListenerRegistration alloc] initWithClient:self
completionHandler:listenerCopy];
}
- (void)removeConfigUpdateListener:(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate,
NSError *_Nullable error))listener {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
[strongSelf->_listeners removeObject:listener];
if (strongSelf->_listeners.count == 0) {
[strongSelf pauseRealtimeStream];
}
});
}
@end