FIRMessagingClient.m 19 KB

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