X-Git-Url: https://git.mdrn.pl/wl-app.git/blobdiff_plain/53b27422d140022594fc241cca91c3183be57bca..48b2fe9f7c2dc3d9aeaaa6dbfb27c7da4f3235ff:/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingClient.m diff --git a/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingClient.m b/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingClient.m new file mode 100644 index 0000000..9d8c558 --- /dev/null +++ b/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingClient.m @@ -0,0 +1,506 @@ +/* + * 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 "FIRMessagingClient.h" + +#import + +#import "FIRMessaging.h" +#import "FIRMessagingConnection.h" +#import "FIRMessagingConstants.h" +#import "FIRMessagingDataMessageManager.h" +#import "FIRMessagingDefines.h" +#import "FIRMessagingLogger.h" +#import "FIRMessagingRegistrar.h" +#import "FIRMessagingRmqManager.h" +#import "FIRMessagingTopicsCommon.h" +#import "FIRMessagingUtilities.h" +#import "NSError+FIRMessaging.h" + +static const NSTimeInterval kConnectTimeoutInterval = 40.0; +static const NSTimeInterval kReconnectDelayInSeconds = 2 * 60; // 2 minutes + +static const NSUInteger kMaxRetryExponent = 10; // 2^10 = 1024 seconds ~= 17 minutes + +static NSString *const kFIRMessagingMCSServerHost = @"mtalk.google.com"; +static NSUInteger const kFIRMessagingMCSServerPort = 5228; + +// register device with checkin +typedef void(^FIRMessagingRegisterDeviceHandler)(NSError *error); + +static NSString *FIRMessagingServerHost() { + static NSString *serverHost = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSDictionary *environment = [[NSProcessInfo processInfo] environment]; + NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"]; + NSString *host = [customServerHostAndPort componentsSeparatedByString:@":"].firstObject; + if (host) { + serverHost = host; + } else { + serverHost = kFIRMessagingMCSServerHost; + } + }); + return serverHost; +} + +static NSUInteger FIRMessagingServerPort() { + static NSUInteger serverPort = kFIRMessagingMCSServerPort; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSDictionary *environment = [[NSProcessInfo processInfo] environment]; + NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"]; + NSArray *components = [customServerHostAndPort componentsSeparatedByString:@":"]; + NSUInteger port = (NSUInteger)[components.lastObject integerValue]; + if (port != 0) { + serverPort = port; + } + }); + return serverPort; +} + +@interface FIRMessagingClient () + +@property(nonatomic, readwrite, weak) id clientDelegate; +@property(nonatomic, readwrite, strong) FIRMessagingConnection *connection; +@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar; + +@property(nonatomic, readwrite, strong) NSString *senderId; + +// FIRMessagingService owns these instances +@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager; +@property(nonatomic, readwrite, weak) GULReachabilityChecker *reachability; + +@property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp; +@property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp; +@property(nonatomic, readwrite, assign) NSUInteger connectRetryCount; + +// Should we stay connected to MCS or not. Should be YES throughout the lifetime +// of a MCS connection. If set to NO it signifies that an existing MCS connection +// should be disconnected. +@property(nonatomic, readwrite, assign) BOOL stayConnected; +@property(nonatomic, readwrite, assign) NSTimeInterval connectionTimeoutInterval; + +// Used if the MCS connection suddenly breaksdown in the middle and we want to reconnect +// with some permissible delay we schedule a reconnect and set it to YES and when it's +// scheduled this will be set back to NO. +@property(nonatomic, readwrite, assign) BOOL didScheduleReconnect; + +// handlers +@property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectHandler; + +@end + +@implementation FIRMessagingClient + +- (instancetype)init { + FIRMessagingInvalidateInitializer(); +} + +- (instancetype)initWithDelegate:(id)delegate + reachability:(GULReachabilityChecker *)reachability + rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager { + self = [super init]; + if (self) { + _reachability = reachability; + _clientDelegate = delegate; + _rmq2Manager = rmq2Manager; + _registrar = [[FIRMessagingRegistrar alloc] init]; + _connectionTimeoutInterval = kConnectTimeoutInterval; + // Listen for checkin fetch notifications, as connecting to MCS may have failed due to + // missing checkin info (while it was being fetched). + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(checkinFetched:) + name:kFIRMessagingCheckinFetchedNotification + object:nil]; + } + return self; +} + +- (void)teardown { + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000, @""); + self.stayConnected = NO; + + // Clear all the handlers + self.connectHandler = nil; + + [self.connection teardown]; + + // Stop all subscription requests + [self.registrar cancelAllRequests]; + + _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect"); + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)cancelAllRequests { + // Stop any checkin requests or any subscription requests + [self.registrar cancelAllRequests]; + + // Stop any future connection requests to MCS + if (self.stayConnected && self.isConnected && !self.isConnectionActive) { + self.stayConnected = NO; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + } +} + +#pragma mark - FIRMessaging subscribe + +- (void)updateSubscriptionWithToken:(NSString *)token + topic:(NSString *)topic + options:(NSDictionary *)options + shouldDelete:(BOOL)shouldDelete + handler:(FIRMessagingTopicOperationCompletion)handler { + + _FIRMessagingDevAssert(handler != nil, @"Invalid handler to FIRMessaging subscribe"); + + FIRMessagingTopicOperationCompletion completion = ^void(NSError *error) { + if (error) { + FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@", + error); + } else { + if (shouldDelete) { + FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002, + @"Successfully unsubscribed from topic %@", topic); + } else { + FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003, + @"Successfully subscribed to topic %@", topic); + } + } + handler(error); + }; + + [self.registrar updateSubscriptionToTopic:topic + withToken:token + options:options + shouldDelete:shouldDelete + handler:completion]; +} + +#pragma mark - MCS Connection + +- (BOOL)isConnected { + return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected; +} + +- (BOOL)isConnectionActive { + return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn; +} + +- (BOOL)shouldStayConnected { + return self.stayConnected; +} + +- (void)retryConnectionImmediately:(BOOL)immediately { + // Do not connect to an invalid host or an invalid port + if (!self.stayConnected || !self.connection.host || self.connection.port == 0) { + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004, + @"FIRMessaging connection will not reconnect to MCS. " + @"Stay connected: %d", + self.stayConnected); + return; + } + if (self.isConnectionActive) { + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005, + @"FIRMessaging Connection skip retry, active"); + // already connected and logged in. + // Heartbeat alarm is set and will force close the connection + return; + } + if (self.isConnected) { + // already connected and logged in. + // Heartbeat alarm is set and will force close the connection + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006, + @"FIRMessaging Connection skip retry, connected"); + return; + } + + if (immediately) { + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007, + @"Try to connect to MCS immediately"); + [self tryToConnect]; + } else { + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily"); + // Avoid all the other logic that we have in other clients, since this would always happen + // when the app is in the foreground and since the FIRMessaging connection isn't shared with any other + // app we can be more aggressive in reconnections + if (!self.didScheduleReconnect) { + FIRMessaging_WEAKIFY(self); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + FIRMessaging_STRONGIFY(self); + self.didScheduleReconnect = NO; + [self tryToConnect]; + }); + + self.didScheduleReconnect = YES; + } + } +} + +- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler { + if (self.isConnected) { + NSError *error = [NSError fcm_errorWithCode:kFIRMessagingErrorCodeAlreadyConnected + userInfo:@{ + NSLocalizedFailureReasonErrorKey: @"FIRMessaging is already connected", + }]; + handler(error); + return; + } + self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds(); + self.connectHandler = handler; + [self connect]; +} + +- (void)connect { + // reset retry counts + self.connectRetryCount = 0; + + if (self.isConnected) { + return; + } + + self.stayConnected = YES; + if (![self.registrar tryToLoadValidCheckinInfo]) { + // Checkin info is not available. This may be due to the checkin still being fetched. + if (self.connectHandler) { + NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID]; + self.connectHandler(error); + } + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009, + @"Failed to connect to MCS. No deviceID and secret found."); + // Return for now. If checkin is, in fact, retrieved, the + // |kFIRMessagingCheckinFetchedNotification| will be fired. + return; + } + [self setupConnectionAndConnect]; +} + +- (void)disconnect { + // user called disconnect + // We don't want to connect later even if no network is available. + [self disconnectWithTryToConnectLater:NO]; +} + +/** + * Disconnect the current client connection. Also explicitly stop and connction retries. + * + * @param tryToConnectLater If YES will try to connect later when sending upstream messages + * else if NO do not connect again until user explicitly calls + * connect. + */ +- (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater { + + self.stayConnected = tryToConnectLater; + [self.connection signOut]; + _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, + @"FIRMessaging connection did not disconnect"); + + // since we can disconnect while still trying to establish the connection it's required to + // cancel all performSelectors else the object might be retained + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(tryToConnect) + object:nil]; + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(didConnectTimeout) + object:nil]; + self.connectHandler = nil; +} + +#pragma mark - Checkin Notification +- (void)checkinFetched:(NSNotification *)notification { + // A failed checkin may have been the reason for the connection failure. Attempt a connection + // if the checkin fetched notification is fired. + if (self.stayConnected && !self.isConnected) { + [self connect]; + } +} + +#pragma mark - Messages + +- (void)sendMessage:(GPBMessage *)message { + [self.connection sendProto:message]; +} + +- (void)sendOnConnectOrDrop:(GPBMessage *)message { + [self.connection sendOnConnectOrDrop:message]; +} + +#pragma mark - FIRMessagingConnectionDelegate + +- (void)connection:(FIRMessagingConnection *)fcmConnection + didCloseForReason:(FIRMessagingConnectionCloseReason)reason { + + self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds(); + + if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) { + // Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in + // failed, due to a connection error caused by bad network. + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(didConnectTimeout) + object:nil]; + } + if (self.stayConnected) { + [self scheduleConnectRetry]; + } +} + +- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection { + // Cancel the not-yet-triggered timeout task. + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(didConnectTimeout) + object:nil]; + self.connectRetryCount = 0; + self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds(); + + + [self.dataMessageManager setDeviceAuthID:self.registrar.deviceAuthID + secretToken:self.registrar.secretToken]; + if (self.connectHandler) { + self.connectHandler(nil); + // notified the third party app with the registrationId. + // we don't want them to know about the connection status and how it changes + // so remove this handler + self.connectHandler = nil; + } +} + +- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message { + NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message]; + if ([parsedMessage count]) { + [self.dataMessageManager didReceiveParsedMessage:parsedMessage]; + } +} + +- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds { + NSSet *rmqIDSet = [NSSet setWithArray:rmqIds]; + NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count]; + [self.rmq2Manager scanWithRmqMessageHandler:nil + dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) { + NSString *rmqIdString = [NSString stringWithFormat:@"%lld", rmqId]; + if ([rmqIDSet containsObject:rmqIdString]) { + [messagesSent addObject:stanza]; + } + }]; + for (GtalkDataMessageStanza *message in messagesSent) { + [self.dataMessageManager didSendDataMessageStanza:message]; + } + return [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds]; +} + +#pragma mark - Private + +- (void)setupConnectionAndConnect { + [self setupConnection]; + [self tryToConnect]; +} + +- (void)setupConnection { + NSString *host = FIRMessagingServerHost(); + NSUInteger port = FIRMessagingServerPort(); + _FIRMessagingDevAssert([host length] > 0 && port != 0, @"Invalid port or host"); + + if (self.connection != nil) { + // if there is an old connection, explicitly sign it off. + [self.connection signOut]; + self.connection.delegate = nil; + } + self.connection = [[FIRMessagingConnection alloc] initWithAuthID:self.registrar.deviceAuthID + token:self.registrar.secretToken + host:host + port:port + runLoop:[NSRunLoop mainRunLoop] + rmq2Manager:self.rmq2Manager + fcmManager:self.dataMessageManager]; + self.connection.delegate = self; +} + +- (void)tryToConnect { + if (!self.stayConnected) { + return; + } + + // Cancel any other pending signin requests. + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(tryToConnect) + object:nil]; + + // Do not re-sign in if there is already a connection in progress. + if (self.connection.state != kFIRMessagingConnectionNotConnected) { + return; + } + + _FIRMessagingDevAssert(self.registrar.deviceAuthID.length > 0 && + self.registrar.secretToken.length > 0 && + self.connection != nil, + @"Invalid state cannot connect"); + + self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1); + [self performSelector:@selector(didConnectTimeout) + withObject:nil + afterDelay:self.connectionTimeoutInterval]; + [self.connection signIn]; +} + +- (void)didConnectTimeout { + _FIRMessagingDevAssert(self.connection.state != kFIRMessagingConnectionSignedIn, + @"Invalid state for MCS connection"); + + if (self.stayConnected) { + [self.connection signOut]; + [self scheduleConnectRetry]; + } +} + +#pragma mark - Schedulers + +- (void)scheduleConnectRetry { + GULReachabilityStatus status = self.reachability.reachabilityStatus; + BOOL isReachable = (status == kGULReachabilityViaWifi || status == kGULReachabilityViaCellular); + if (!isReachable) { + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010, + @"Internet not reachable when signing into MCS during a retry"); + + FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy]; + // disconnect before issuing a callback + [self disconnectWithTryToConnectLater:YES]; + NSError *error = + [NSError errorWithDomain:@"No internet available, cannot connect to FIRMessaging" + code:kFIRMessagingErrorCodeNetwork + userInfo:nil]; + if (handler) { + handler(error); + self.connectHandler = nil; + } + return; + } + + NSUInteger retryInterval = [self nextRetryInterval]; + + FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011, + @"Failed to sign in to MCS, retry in %lu seconds", + _FIRMessaging_UL(retryInterval)); + [self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval]; +} + +- (NSUInteger)nextRetryInterval { + return 1u << self.connectRetryCount; +} + +@end