You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

506 lines
19 KiB

6 years ago
  1. /*
  2. * Copyright 2017 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 "FIRMessagingClient.h"
  17. #import <FirebaseCore/FIRReachabilityChecker.h>
  18. #import "FIRMessaging.h"
  19. #import "FIRMessagingConnection.h"
  20. #import "FIRMessagingConstants.h"
  21. #import "FIRMessagingDataMessageManager.h"
  22. #import "FIRMessagingDefines.h"
  23. #import "FIRMessagingLogger.h"
  24. #import "FIRMessagingRegistrar.h"
  25. #import "FIRMessagingRmqManager.h"
  26. #import "FIRMessagingTopicsCommon.h"
  27. #import "FIRMessagingUtilities.h"
  28. #import "NSError+FIRMessaging.h"
  29. static const NSTimeInterval kConnectTimeoutInterval = 40.0;
  30. static const NSTimeInterval kReconnectDelayInSeconds = 2 * 60; // 2 minutes
  31. static const NSUInteger kMaxRetryExponent = 10; // 2^10 = 1024 seconds ~= 17 minutes
  32. static NSString *const kFIRMessagingMCSServerHost = @"mtalk.google.com";
  33. static NSUInteger const kFIRMessagingMCSServerPort = 5228;
  34. // register device with checkin
  35. typedef void(^FIRMessagingRegisterDeviceHandler)(NSError *error);
  36. static NSString *FIRMessagingServerHost() {
  37. static NSString *serverHost = nil;
  38. static dispatch_once_t onceToken;
  39. dispatch_once(&onceToken, ^{
  40. NSDictionary *environment = [[NSProcessInfo processInfo] environment];
  41. NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
  42. NSString *host = [customServerHostAndPort componentsSeparatedByString:@":"].firstObject;
  43. if (host) {
  44. serverHost = host;
  45. } else {
  46. serverHost = kFIRMessagingMCSServerHost;
  47. }
  48. });
  49. return serverHost;
  50. }
  51. static NSUInteger FIRMessagingServerPort() {
  52. static NSUInteger serverPort = kFIRMessagingMCSServerPort;
  53. static dispatch_once_t onceToken;
  54. dispatch_once(&onceToken, ^{
  55. NSDictionary *environment = [[NSProcessInfo processInfo] environment];
  56. NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
  57. NSArray<NSString *> *components = [customServerHostAndPort componentsSeparatedByString:@":"];
  58. NSUInteger port = (NSUInteger)[components.lastObject integerValue];
  59. if (port != 0) {
  60. serverPort = port;
  61. }
  62. });
  63. return serverPort;
  64. }
  65. @interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
  66. @property(nonatomic, readwrite, weak) id<FIRMessagingClientDelegate> clientDelegate;
  67. @property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
  68. @property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
  69. @property(nonatomic, readwrite, strong) NSString *senderId;
  70. // FIRMessagingService owns these instances
  71. @property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
  72. @property(nonatomic, readwrite, weak) FIRReachabilityChecker *reachability;
  73. @property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
  74. @property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
  75. @property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
  76. // Should we stay connected to MCS or not. Should be YES throughout the lifetime
  77. // of a MCS connection. If set to NO it signifies that an existing MCS connection
  78. // should be disconnected.
  79. @property(nonatomic, readwrite, assign) BOOL stayConnected;
  80. @property(nonatomic, readwrite, assign) NSTimeInterval connectionTimeoutInterval;
  81. // Used if the MCS connection suddenly breaksdown in the middle and we want to reconnect
  82. // with some permissible delay we schedule a reconnect and set it to YES and when it's
  83. // scheduled this will be set back to NO.
  84. @property(nonatomic, readwrite, assign) BOOL didScheduleReconnect;
  85. // handlers
  86. @property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectHandler;
  87. @end
  88. @implementation FIRMessagingClient
  89. - (instancetype)init {
  90. FIRMessagingInvalidateInitializer();
  91. }
  92. - (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
  93. reachability:(FIRReachabilityChecker *)reachability
  94. rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
  95. self = [super init];
  96. if (self) {
  97. _reachability = reachability;
  98. _clientDelegate = delegate;
  99. _rmq2Manager = rmq2Manager;
  100. _registrar = [[FIRMessagingRegistrar alloc] init];
  101. _connectionTimeoutInterval = kConnectTimeoutInterval;
  102. // Listen for checkin fetch notifications, as connecting to MCS may have failed due to
  103. // missing checkin info (while it was being fetched).
  104. [[NSNotificationCenter defaultCenter] addObserver:self
  105. selector:@selector(checkinFetched:)
  106. name:kFIRMessagingCheckinFetchedNotification
  107. object:nil];
  108. }
  109. return self;
  110. }
  111. - (void)teardown {
  112. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000, @"");
  113. self.stayConnected = NO;
  114. // Clear all the handlers
  115. self.connectHandler = nil;
  116. [self.connection teardown];
  117. // Stop all subscription requests
  118. [self.registrar cancelAllRequests];
  119. _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect");
  120. [NSObject cancelPreviousPerformRequestsWithTarget:self];
  121. [[NSNotificationCenter defaultCenter] removeObserver:self];
  122. }
  123. - (void)cancelAllRequests {
  124. // Stop any checkin requests or any subscription requests
  125. [self.registrar cancelAllRequests];
  126. // Stop any future connection requests to MCS
  127. if (self.stayConnected && self.isConnected && !self.isConnectionActive) {
  128. self.stayConnected = NO;
  129. [NSObject cancelPreviousPerformRequestsWithTarget:self];
  130. }
  131. }
  132. #pragma mark - FIRMessaging subscribe
  133. - (void)updateSubscriptionWithToken:(NSString *)token
  134. topic:(NSString *)topic
  135. options:(NSDictionary *)options
  136. shouldDelete:(BOOL)shouldDelete
  137. handler:(FIRMessagingTopicOperationCompletion)handler {
  138. _FIRMessagingDevAssert(handler != nil, @"Invalid handler to FIRMessaging subscribe");
  139. FIRMessagingTopicOperationCompletion completion = ^void(NSError *error) {
  140. if (error) {
  141. FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@",
  142. error);
  143. } else {
  144. if (shouldDelete) {
  145. FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
  146. @"Successfully unsubscribed from topic %@", topic);
  147. } else {
  148. FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
  149. @"Successfully subscribed to topic %@", topic);
  150. }
  151. }
  152. handler(error);
  153. };
  154. [self.registrar updateSubscriptionToTopic:topic
  155. withToken:token
  156. options:options
  157. shouldDelete:shouldDelete
  158. handler:completion];
  159. }
  160. #pragma mark - MCS Connection
  161. - (BOOL)isConnected {
  162. return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected;
  163. }
  164. - (BOOL)isConnectionActive {
  165. return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn;
  166. }
  167. - (BOOL)shouldStayConnected {
  168. return self.stayConnected;
  169. }
  170. - (void)retryConnectionImmediately:(BOOL)immediately {
  171. // Do not connect to an invalid host or an invalid port
  172. if (!self.stayConnected || !self.connection.host || self.connection.port == 0) {
  173. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004,
  174. @"FIRMessaging connection will not reconnect to MCS. "
  175. @"Stay connected: %d",
  176. self.stayConnected);
  177. return;
  178. }
  179. if (self.isConnectionActive) {
  180. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005,
  181. @"FIRMessaging Connection skip retry, active");
  182. // already connected and logged in.
  183. // Heartbeat alarm is set and will force close the connection
  184. return;
  185. }
  186. if (self.isConnected) {
  187. // already connected and logged in.
  188. // Heartbeat alarm is set and will force close the connection
  189. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006,
  190. @"FIRMessaging Connection skip retry, connected");
  191. return;
  192. }
  193. if (immediately) {
  194. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007,
  195. @"Try to connect to MCS immediately");
  196. [self tryToConnect];
  197. } else {
  198. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily");
  199. // Avoid all the other logic that we have in other clients, since this would always happen
  200. // when the app is in the foreground and since the FIRMessaging connection isn't shared with any other
  201. // app we can be more aggressive in reconnections
  202. if (!self.didScheduleReconnect) {
  203. FIRMessaging_WEAKIFY(self);
  204. dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
  205. (int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)),
  206. dispatch_get_main_queue(), ^{
  207. FIRMessaging_STRONGIFY(self);
  208. self.didScheduleReconnect = NO;
  209. [self tryToConnect];
  210. });
  211. self.didScheduleReconnect = YES;
  212. }
  213. }
  214. }
  215. - (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler {
  216. if (self.isConnected) {
  217. NSError *error = [NSError fcm_errorWithCode:kFIRMessagingErrorCodeAlreadyConnected
  218. userInfo:@{
  219. NSLocalizedFailureReasonErrorKey: @"FIRMessaging is already connected",
  220. }];
  221. handler(error);
  222. return;
  223. }
  224. self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
  225. self.connectHandler = handler;
  226. [self connect];
  227. }
  228. - (void)connect {
  229. // reset retry counts
  230. self.connectRetryCount = 0;
  231. if (self.isConnected) {
  232. return;
  233. }
  234. self.stayConnected = YES;
  235. if (![self.registrar tryToLoadValidCheckinInfo]) {
  236. // Checkin info is not available. This may be due to the checkin still being fetched.
  237. if (self.connectHandler) {
  238. NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
  239. self.connectHandler(error);
  240. }
  241. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009,
  242. @"Failed to connect to MCS. No deviceID and secret found.");
  243. // Return for now. If checkin is, in fact, retrieved, the
  244. // |kFIRMessagingCheckinFetchedNotification| will be fired.
  245. return;
  246. }
  247. [self setupConnectionAndConnect];
  248. }
  249. - (void)disconnect {
  250. // user called disconnect
  251. // We don't want to connect later even if no network is available.
  252. [self disconnectWithTryToConnectLater:NO];
  253. }
  254. /**
  255. * Disconnect the current client connection. Also explicitly stop and connction retries.
  256. *
  257. * @param tryToConnectLater If YES will try to connect later when sending upstream messages
  258. * else if NO do not connect again until user explicitly calls
  259. * connect.
  260. */
  261. - (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater {
  262. self.stayConnected = tryToConnectLater;
  263. [self.connection signOut];
  264. _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected,
  265. @"FIRMessaging connection did not disconnect");
  266. // since we can disconnect while still trying to establish the connection it's required to
  267. // cancel all performSelectors else the object might be retained
  268. [NSObject cancelPreviousPerformRequestsWithTarget:self
  269. selector:@selector(tryToConnect)
  270. object:nil];
  271. [NSObject cancelPreviousPerformRequestsWithTarget:self
  272. selector:@selector(didConnectTimeout)
  273. object:nil];
  274. self.connectHandler = nil;
  275. }
  276. #pragma mark - Checkin Notification
  277. - (void)checkinFetched:(NSNotification *)notification {
  278. // A failed checkin may have been the reason for the connection failure. Attempt a connection
  279. // if the checkin fetched notification is fired.
  280. if (self.stayConnected && !self.isConnected) {
  281. [self connect];
  282. }
  283. }
  284. #pragma mark - Messages
  285. - (void)sendMessage:(GPBMessage *)message {
  286. [self.connection sendProto:message];
  287. }
  288. - (void)sendOnConnectOrDrop:(GPBMessage *)message {
  289. [self.connection sendOnConnectOrDrop:message];
  290. }
  291. #pragma mark - FIRMessagingConnectionDelegate
  292. - (void)connection:(FIRMessagingConnection *)fcmConnection
  293. didCloseForReason:(FIRMessagingConnectionCloseReason)reason {
  294. self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
  295. if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) {
  296. // Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in
  297. // failed, due to a connection error caused by bad network.
  298. [NSObject cancelPreviousPerformRequestsWithTarget:self
  299. selector:@selector(didConnectTimeout)
  300. object:nil];
  301. }
  302. if (self.stayConnected) {
  303. [self scheduleConnectRetry];
  304. }
  305. }
  306. - (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection {
  307. // Cancel the not-yet-triggered timeout task.
  308. [NSObject cancelPreviousPerformRequestsWithTarget:self
  309. selector:@selector(didConnectTimeout)
  310. object:nil];
  311. self.connectRetryCount = 0;
  312. self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
  313. [self.dataMessageManager setDeviceAuthID:self.registrar.deviceAuthID
  314. secretToken:self.registrar.secretToken];
  315. if (self.connectHandler) {
  316. self.connectHandler(nil);
  317. // notified the third party app with the registrationId.
  318. // we don't want them to know about the connection status and how it changes
  319. // so remove this handler
  320. self.connectHandler = nil;
  321. }
  322. }
  323. - (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message {
  324. NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
  325. if ([parsedMessage count]) {
  326. [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
  327. }
  328. }
  329. - (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds {
  330. NSSet *rmqIDSet = [NSSet setWithArray:rmqIds];
  331. NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count];
  332. [self.rmq2Manager scanWithRmqMessageHandler:nil
  333. dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
  334. NSString *rmqIdString = [NSString stringWithFormat:@"%lld", rmqId];
  335. if ([rmqIDSet containsObject:rmqIdString]) {
  336. [messagesSent addObject:stanza];
  337. }
  338. }];
  339. for (GtalkDataMessageStanza *message in messagesSent) {
  340. [self.dataMessageManager didSendDataMessageStanza:message];
  341. }
  342. return [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds];
  343. }
  344. #pragma mark - Private
  345. - (void)setupConnectionAndConnect {
  346. [self setupConnection];
  347. [self tryToConnect];
  348. }
  349. - (void)setupConnection {
  350. NSString *host = FIRMessagingServerHost();
  351. NSUInteger port = FIRMessagingServerPort();
  352. _FIRMessagingDevAssert([host length] > 0 && port != 0, @"Invalid port or host");
  353. if (self.connection != nil) {
  354. // if there is an old connection, explicitly sign it off.
  355. [self.connection signOut];
  356. self.connection.delegate = nil;
  357. }
  358. self.connection = [[FIRMessagingConnection alloc] initWithAuthID:self.registrar.deviceAuthID
  359. token:self.registrar.secretToken
  360. host:host
  361. port:port
  362. runLoop:[NSRunLoop mainRunLoop]
  363. rmq2Manager:self.rmq2Manager
  364. fcmManager:self.dataMessageManager];
  365. self.connection.delegate = self;
  366. }
  367. - (void)tryToConnect {
  368. if (!self.stayConnected) {
  369. return;
  370. }
  371. // Cancel any other pending signin requests.
  372. [NSObject cancelPreviousPerformRequestsWithTarget:self
  373. selector:@selector(tryToConnect)
  374. object:nil];
  375. // Do not re-sign in if there is already a connection in progress.
  376. if (self.connection.state != kFIRMessagingConnectionNotConnected) {
  377. return;
  378. }
  379. _FIRMessagingDevAssert(self.registrar.deviceAuthID.length > 0 &&
  380. self.registrar.secretToken.length > 0 &&
  381. self.connection != nil,
  382. @"Invalid state cannot connect");
  383. self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1);
  384. [self performSelector:@selector(didConnectTimeout)
  385. withObject:nil
  386. afterDelay:self.connectionTimeoutInterval];
  387. [self.connection signIn];
  388. }
  389. - (void)didConnectTimeout {
  390. _FIRMessagingDevAssert(self.connection.state != kFIRMessagingConnectionSignedIn,
  391. @"Invalid state for MCS connection");
  392. if (self.stayConnected) {
  393. [self.connection signOut];
  394. [self scheduleConnectRetry];
  395. }
  396. }
  397. #pragma mark - Schedulers
  398. - (void)scheduleConnectRetry {
  399. FIRReachabilityStatus status = self.reachability.reachabilityStatus;
  400. BOOL isReachable = (status == kFIRReachabilityViaWifi || status == kFIRReachabilityViaCellular);
  401. if (!isReachable) {
  402. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010,
  403. @"Internet not reachable when signing into MCS during a retry");
  404. FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy];
  405. // disconnect before issuing a callback
  406. [self disconnectWithTryToConnectLater:YES];
  407. NSError *error =
  408. [NSError errorWithDomain:@"No internet available, cannot connect to FIRMessaging"
  409. code:kFIRMessagingErrorCodeNetwork
  410. userInfo:nil];
  411. if (handler) {
  412. handler(error);
  413. self.connectHandler = nil;
  414. }
  415. return;
  416. }
  417. NSUInteger retryInterval = [self nextRetryInterval];
  418. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011,
  419. @"Failed to sign in to MCS, retry in %lu seconds",
  420. _FIRMessaging_UL(retryInterval));
  421. [self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval];
  422. }
  423. - (NSUInteger)nextRetryInterval {
  424. return 1u << self.connectRetryCount;
  425. }
  426. @end