2 * Copyright 2017 Google
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 #import "FIRMessagingClient.h"
19 #import <GoogleUtilities/GULReachabilityChecker.h>
21 #import "FIRMessaging.h"
22 #import "FIRMessagingConnection.h"
23 #import "FIRMessagingConstants.h"
24 #import "FIRMessagingDataMessageManager.h"
25 #import "FIRMessagingDefines.h"
26 #import "FIRMessagingLogger.h"
27 #import "FIRMessagingRegistrar.h"
28 #import "FIRMessagingRmqManager.h"
29 #import "FIRMessagingTopicsCommon.h"
30 #import "FIRMessagingUtilities.h"
31 #import "NSError+FIRMessaging.h"
33 static const NSTimeInterval kConnectTimeoutInterval = 40.0;
34 static const NSTimeInterval kReconnectDelayInSeconds = 2 * 60; // 2 minutes
36 static const NSUInteger kMaxRetryExponent = 10; // 2^10 = 1024 seconds ~= 17 minutes
38 static NSString *const kFIRMessagingMCSServerHost = @"mtalk.google.com";
39 static NSUInteger const kFIRMessagingMCSServerPort = 5228;
41 // register device with checkin
42 typedef void(^FIRMessagingRegisterDeviceHandler)(NSError *error);
44 static NSString *FIRMessagingServerHost() {
45 static NSString *serverHost = nil;
46 static dispatch_once_t onceToken;
47 dispatch_once(&onceToken, ^{
48 NSDictionary *environment = [[NSProcessInfo processInfo] environment];
49 NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
50 NSString *host = [customServerHostAndPort componentsSeparatedByString:@":"].firstObject;
54 serverHost = kFIRMessagingMCSServerHost;
60 static NSUInteger FIRMessagingServerPort() {
61 static NSUInteger serverPort = kFIRMessagingMCSServerPort;
62 static dispatch_once_t onceToken;
63 dispatch_once(&onceToken, ^{
64 NSDictionary *environment = [[NSProcessInfo processInfo] environment];
65 NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
66 NSArray<NSString *> *components = [customServerHostAndPort componentsSeparatedByString:@":"];
67 NSUInteger port = (NSUInteger)[components.lastObject integerValue];
75 @interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
77 @property(nonatomic, readwrite, weak) id<FIRMessagingClientDelegate> clientDelegate;
78 @property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
79 @property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
81 @property(nonatomic, readwrite, strong) NSString *senderId;
83 // FIRMessagingService owns these instances
84 @property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
85 @property(nonatomic, readwrite, weak) GULReachabilityChecker *reachability;
87 @property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
88 @property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
89 @property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
91 // Should we stay connected to MCS or not. Should be YES throughout the lifetime
92 // of a MCS connection. If set to NO it signifies that an existing MCS connection
93 // should be disconnected.
94 @property(nonatomic, readwrite, assign) BOOL stayConnected;
95 @property(nonatomic, readwrite, assign) NSTimeInterval connectionTimeoutInterval;
97 // Used if the MCS connection suddenly breaksdown in the middle and we want to reconnect
98 // with some permissible delay we schedule a reconnect and set it to YES and when it's
99 // scheduled this will be set back to NO.
100 @property(nonatomic, readwrite, assign) BOOL didScheduleReconnect;
103 @property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectHandler;
107 @implementation FIRMessagingClient
109 - (instancetype)init {
110 FIRMessagingInvalidateInitializer();
113 - (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
114 reachability:(GULReachabilityChecker *)reachability
115 rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
118 _reachability = reachability;
119 _clientDelegate = delegate;
120 _rmq2Manager = rmq2Manager;
121 _registrar = [[FIRMessagingRegistrar alloc] init];
122 _connectionTimeoutInterval = kConnectTimeoutInterval;
123 // Listen for checkin fetch notifications, as connecting to MCS may have failed due to
124 // missing checkin info (while it was being fetched).
125 [[NSNotificationCenter defaultCenter] addObserver:self
126 selector:@selector(checkinFetched:)
127 name:kFIRMessagingCheckinFetchedNotification
134 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000, @"");
135 self.stayConnected = NO;
137 // Clear all the handlers
138 self.connectHandler = nil;
140 [self.connection teardown];
142 // Stop all subscription requests
143 [self.registrar cancelAllRequests];
145 _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect");
146 [NSObject cancelPreviousPerformRequestsWithTarget:self];
148 [[NSNotificationCenter defaultCenter] removeObserver:self];
151 - (void)cancelAllRequests {
152 // Stop any checkin requests or any subscription requests
153 [self.registrar cancelAllRequests];
155 // Stop any future connection requests to MCS
156 if (self.stayConnected && self.isConnected && !self.isConnectionActive) {
157 self.stayConnected = NO;
158 [NSObject cancelPreviousPerformRequestsWithTarget:self];
162 #pragma mark - FIRMessaging subscribe
164 - (void)updateSubscriptionWithToken:(NSString *)token
165 topic:(NSString *)topic
166 options:(NSDictionary *)options
167 shouldDelete:(BOOL)shouldDelete
168 handler:(FIRMessagingTopicOperationCompletion)handler {
170 _FIRMessagingDevAssert(handler != nil, @"Invalid handler to FIRMessaging subscribe");
172 FIRMessagingTopicOperationCompletion completion = ^void(NSError *error) {
174 FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@",
178 FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
179 @"Successfully unsubscribed from topic %@", topic);
181 FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
182 @"Successfully subscribed to topic %@", topic);
188 [self.registrar updateSubscriptionToTopic:topic
191 shouldDelete:shouldDelete
195 #pragma mark - MCS Connection
197 - (BOOL)isConnected {
198 return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected;
201 - (BOOL)isConnectionActive {
202 return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn;
205 - (BOOL)shouldStayConnected {
206 return self.stayConnected;
209 - (void)retryConnectionImmediately:(BOOL)immediately {
210 // Do not connect to an invalid host or an invalid port
211 if (!self.stayConnected || !self.connection.host || self.connection.port == 0) {
212 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004,
213 @"FIRMessaging connection will not reconnect to MCS. "
214 @"Stay connected: %d",
218 if (self.isConnectionActive) {
219 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005,
220 @"FIRMessaging Connection skip retry, active");
221 // already connected and logged in.
222 // Heartbeat alarm is set and will force close the connection
225 if (self.isConnected) {
226 // already connected and logged in.
227 // Heartbeat alarm is set and will force close the connection
228 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006,
229 @"FIRMessaging Connection skip retry, connected");
234 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007,
235 @"Try to connect to MCS immediately");
238 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily");
239 // Avoid all the other logic that we have in other clients, since this would always happen
240 // when the app is in the foreground and since the FIRMessaging connection isn't shared with any other
241 // app we can be more aggressive in reconnections
242 if (!self.didScheduleReconnect) {
243 FIRMessaging_WEAKIFY(self);
244 dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
245 (int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)),
246 dispatch_get_main_queue(), ^{
247 FIRMessaging_STRONGIFY(self);
248 self.didScheduleReconnect = NO;
252 self.didScheduleReconnect = YES;
257 - (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler {
258 if (self.isConnected) {
259 NSError *error = [NSError fcm_errorWithCode:kFIRMessagingErrorCodeAlreadyConnected
261 NSLocalizedFailureReasonErrorKey: @"FIRMessaging is already connected",
266 self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
267 self.connectHandler = handler;
272 // reset retry counts
273 self.connectRetryCount = 0;
275 if (self.isConnected) {
279 self.stayConnected = YES;
280 if (![self.registrar tryToLoadValidCheckinInfo]) {
281 // Checkin info is not available. This may be due to the checkin still being fetched.
282 if (self.connectHandler) {
283 NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
284 self.connectHandler(error);
286 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009,
287 @"Failed to connect to MCS. No deviceID and secret found.");
288 // Return for now. If checkin is, in fact, retrieved, the
289 // |kFIRMessagingCheckinFetchedNotification| will be fired.
292 [self setupConnectionAndConnect];
296 // user called disconnect
297 // We don't want to connect later even if no network is available.
298 [self disconnectWithTryToConnectLater:NO];
302 * Disconnect the current client connection. Also explicitly stop and connction retries.
304 * @param tryToConnectLater If YES will try to connect later when sending upstream messages
305 * else if NO do not connect again until user explicitly calls
308 - (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater {
310 self.stayConnected = tryToConnectLater;
311 [self.connection signOut];
312 _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected,
313 @"FIRMessaging connection did not disconnect");
315 // since we can disconnect while still trying to establish the connection it's required to
316 // cancel all performSelectors else the object might be retained
317 [NSObject cancelPreviousPerformRequestsWithTarget:self
318 selector:@selector(tryToConnect)
320 [NSObject cancelPreviousPerformRequestsWithTarget:self
321 selector:@selector(didConnectTimeout)
323 self.connectHandler = nil;
326 #pragma mark - Checkin Notification
327 - (void)checkinFetched:(NSNotification *)notification {
328 // A failed checkin may have been the reason for the connection failure. Attempt a connection
329 // if the checkin fetched notification is fired.
330 if (self.stayConnected && !self.isConnected) {
335 #pragma mark - Messages
337 - (void)sendMessage:(GPBMessage *)message {
338 [self.connection sendProto:message];
341 - (void)sendOnConnectOrDrop:(GPBMessage *)message {
342 [self.connection sendOnConnectOrDrop:message];
345 #pragma mark - FIRMessagingConnectionDelegate
347 - (void)connection:(FIRMessagingConnection *)fcmConnection
348 didCloseForReason:(FIRMessagingConnectionCloseReason)reason {
350 self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
352 if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) {
353 // Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in
354 // failed, due to a connection error caused by bad network.
355 [NSObject cancelPreviousPerformRequestsWithTarget:self
356 selector:@selector(didConnectTimeout)
359 if (self.stayConnected) {
360 [self scheduleConnectRetry];
364 - (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection {
365 // Cancel the not-yet-triggered timeout task.
366 [NSObject cancelPreviousPerformRequestsWithTarget:self
367 selector:@selector(didConnectTimeout)
369 self.connectRetryCount = 0;
370 self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
373 [self.dataMessageManager setDeviceAuthID:self.registrar.deviceAuthID
374 secretToken:self.registrar.secretToken];
375 if (self.connectHandler) {
376 self.connectHandler(nil);
377 // notified the third party app with the registrationId.
378 // we don't want them to know about the connection status and how it changes
379 // so remove this handler
380 self.connectHandler = nil;
384 - (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message {
385 NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
386 if ([parsedMessage count]) {
387 [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
391 - (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds {
392 NSSet *rmqIDSet = [NSSet setWithArray:rmqIds];
393 NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count];
394 [self.rmq2Manager scanWithRmqMessageHandler:nil
395 dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
396 NSString *rmqIdString = [NSString stringWithFormat:@"%lld", rmqId];
397 if ([rmqIDSet containsObject:rmqIdString]) {
398 [messagesSent addObject:stanza];
401 for (GtalkDataMessageStanza *message in messagesSent) {
402 [self.dataMessageManager didSendDataMessageStanza:message];
404 return [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds];
407 #pragma mark - Private
409 - (void)setupConnectionAndConnect {
410 [self setupConnection];
414 - (void)setupConnection {
415 NSString *host = FIRMessagingServerHost();
416 NSUInteger port = FIRMessagingServerPort();
417 _FIRMessagingDevAssert([host length] > 0 && port != 0, @"Invalid port or host");
419 if (self.connection != nil) {
420 // if there is an old connection, explicitly sign it off.
421 [self.connection signOut];
422 self.connection.delegate = nil;
424 self.connection = [[FIRMessagingConnection alloc] initWithAuthID:self.registrar.deviceAuthID
425 token:self.registrar.secretToken
428 runLoop:[NSRunLoop mainRunLoop]
429 rmq2Manager:self.rmq2Manager
430 fcmManager:self.dataMessageManager];
431 self.connection.delegate = self;
434 - (void)tryToConnect {
435 if (!self.stayConnected) {
439 // Cancel any other pending signin requests.
440 [NSObject cancelPreviousPerformRequestsWithTarget:self
441 selector:@selector(tryToConnect)
444 // Do not re-sign in if there is already a connection in progress.
445 if (self.connection.state != kFIRMessagingConnectionNotConnected) {
449 _FIRMessagingDevAssert(self.registrar.deviceAuthID.length > 0 &&
450 self.registrar.secretToken.length > 0 &&
451 self.connection != nil,
452 @"Invalid state cannot connect");
454 self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1);
455 [self performSelector:@selector(didConnectTimeout)
457 afterDelay:self.connectionTimeoutInterval];
458 [self.connection signIn];
461 - (void)didConnectTimeout {
462 _FIRMessagingDevAssert(self.connection.state != kFIRMessagingConnectionSignedIn,
463 @"Invalid state for MCS connection");
465 if (self.stayConnected) {
466 [self.connection signOut];
467 [self scheduleConnectRetry];
471 #pragma mark - Schedulers
473 - (void)scheduleConnectRetry {
474 GULReachabilityStatus status = self.reachability.reachabilityStatus;
475 BOOL isReachable = (status == kGULReachabilityViaWifi || status == kGULReachabilityViaCellular);
477 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010,
478 @"Internet not reachable when signing into MCS during a retry");
480 FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy];
481 // disconnect before issuing a callback
482 [self disconnectWithTryToConnectLater:YES];
484 [NSError errorWithDomain:@"No internet available, cannot connect to FIRMessaging"
485 code:kFIRMessagingErrorCodeNetwork
489 self.connectHandler = nil;
494 NSUInteger retryInterval = [self nextRetryInterval];
496 FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011,
497 @"Failed to sign in to MCS, retry in %lu seconds",
498 _FIRMessaging_UL(retryInterval));
499 [self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval];
502 - (NSUInteger)nextRetryInterval {
503 return 1u << self.connectRetryCount;