FIRMessagingClient.m 18 KB

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