FIRMessagingClient.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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 "FirebaseMessaging/Sources/FIRMessagingClient.h"
  17. #import "FirebaseMessaging/Sources/Protos/GtalkCore.pbobjc.h"
  18. #import <FirebaseInstanceID/FIRInstanceID_Private.h>
  19. #import <FirebaseMessaging/FIRMessaging.h>
  20. #import "GoogleUtilities/Reachability/Private/GULReachabilityChecker.h"
  21. #import "FirebaseMessaging/Sources/FIRMessagingConnection.h"
  22. #import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
  23. #import "FirebaseMessaging/Sources/FIRMessagingDataMessageManager.h"
  24. #import "FirebaseMessaging/Sources/FIRMessagingDefines.h"
  25. #import "FirebaseMessaging/Sources/FIRMessagingLogger.h"
  26. #import "FirebaseMessaging/Sources/FIRMessagingRmqManager.h"
  27. #import "FirebaseMessaging/Sources/FIRMessagingTopicsCommon.h"
  28. #import "FirebaseMessaging/Sources/FIRMessagingUtilities.h"
  29. #import "FirebaseMessaging/Sources/NSError+FIRMessaging.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, readwrite, strong) NSString *senderId;
  70. // FIRMessagingService owns these instances
  71. @property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
  72. @property(nonatomic, readwrite, weak) GULReachabilityChecker *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:(GULReachabilityChecker *)reachability
  94. rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
  95. self = [super init];
  96. if (self) {
  97. _reachability = reachability;
  98. _clientDelegate = delegate;
  99. _rmq2Manager = rmq2Manager;
  100. _connectionTimeoutInterval = kConnectTimeoutInterval;
  101. // Listen for checkin fetch notifications, as connecting to MCS may have failed due to
  102. // missing checkin info (while it was being fetched).
  103. [[NSNotificationCenter defaultCenter] addObserver:self
  104. selector:@selector(checkinFetched:)
  105. name:kFIRMessagingCheckinFetchedNotification
  106. object:nil];
  107. }
  108. return self;
  109. }
  110. - (void)teardown {
  111. if (![NSThread isMainThread]) {
  112. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000,
  113. @"FIRMessagingClient should be called from main thread only.");
  114. }
  115. self.stayConnected = NO;
  116. // Clear all the handlers
  117. self.connectHandler = nil;
  118. [self.connection teardown];
  119. [NSObject cancelPreviousPerformRequestsWithTarget:self];
  120. [[NSNotificationCenter defaultCenter] removeObserver:self];
  121. }
  122. #pragma mark - MCS Connection
  123. - (BOOL)isConnected {
  124. return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected;
  125. }
  126. - (BOOL)isConnectionActive {
  127. return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn;
  128. }
  129. - (BOOL)shouldStayConnected {
  130. return self.stayConnected;
  131. }
  132. - (void)retryConnectionImmediately:(BOOL)immediately {
  133. // Do not connect to an invalid host or an invalid port
  134. if (!self.stayConnected || !self.connection.host || self.connection.port == 0) {
  135. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004,
  136. @"FIRMessaging connection will not reconnect to MCS. "
  137. @"Stay connected: %d",
  138. self.stayConnected);
  139. return;
  140. }
  141. if (self.isConnectionActive) {
  142. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005,
  143. @"FIRMessaging Connection skip retry, active");
  144. // already connected and logged in.
  145. // Heartbeat alarm is set and will force close the connection
  146. return;
  147. }
  148. if (self.isConnected) {
  149. // already connected and logged in.
  150. // Heartbeat alarm is set and will force close the connection
  151. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006,
  152. @"FIRMessaging Connection skip retry, connected");
  153. return;
  154. }
  155. if (immediately) {
  156. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007,
  157. @"Try to connect to MCS immediately");
  158. [self tryToConnect];
  159. } else {
  160. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily");
  161. // Avoid all the other logic that we have in other clients, since this would always happen
  162. // when the app is in the foreground and since the FIRMessaging connection isn't shared with any
  163. // other app we can be more aggressive in reconnections
  164. if (!self.didScheduleReconnect) {
  165. FIRMessaging_WEAKIFY(self);
  166. dispatch_after(
  167. dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)),
  168. dispatch_get_main_queue(), ^{
  169. FIRMessaging_STRONGIFY(self);
  170. self.didScheduleReconnect = NO;
  171. [self tryToConnect];
  172. });
  173. self.didScheduleReconnect = YES;
  174. }
  175. }
  176. }
  177. - (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler {
  178. if (self.isConnected) {
  179. NSError *error =
  180. [NSError messagingErrorWithCode:kFIRMessagingErrorCodeAlreadyConnected
  181. failureReason:
  182. @"FIRMessaging is already connected. Will not try to connect again."];
  183. handler(error);
  184. return;
  185. }
  186. self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
  187. self.connectHandler = handler;
  188. [self connect];
  189. }
  190. - (void)connect {
  191. // reset retry counts
  192. self.connectRetryCount = 0;
  193. if (self.isConnected) {
  194. return;
  195. }
  196. self.stayConnected = YES;
  197. if (![[FIRInstanceID instanceID] tryToLoadValidCheckinInfo]) {
  198. // Checkin info is not available. This may be due to the checkin still being fetched.
  199. NSString *failureReason = @"Failed to connect to MCS. No deviceID and secret found.";
  200. if (self.connectHandler) {
  201. NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeMissingDeviceID
  202. failureReason:failureReason];
  203. self.connectHandler(error);
  204. }
  205. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009, @"%@", failureReason);
  206. // Return for now. If checkin is, in fact, retrieved, the
  207. // |kFIRMessagingCheckinFetchedNotification| will be fired.
  208. return;
  209. }
  210. [self setupConnectionAndConnect];
  211. }
  212. - (void)disconnect {
  213. // user called disconnect
  214. // We don't want to connect later even if no network is available.
  215. [self disconnectWithTryToConnectLater:NO];
  216. }
  217. /**
  218. * Disconnect the current client connection. Also explicitly stop and connction retries.
  219. *
  220. * @param tryToConnectLater If YES will try to connect later when sending upstream messages
  221. * else if NO do not connect again until user explicitly calls
  222. * connect.
  223. */
  224. - (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater {
  225. self.stayConnected = tryToConnectLater;
  226. [self.connection signOut];
  227. // since we can disconnect while still trying to establish the connection it's required to
  228. // cancel all performSelectors else the object might be retained
  229. [NSObject cancelPreviousPerformRequestsWithTarget:self
  230. selector:@selector(tryToConnect)
  231. object:nil];
  232. [NSObject cancelPreviousPerformRequestsWithTarget:self
  233. selector:@selector(didConnectTimeout)
  234. object:nil];
  235. self.connectHandler = nil;
  236. }
  237. #pragma mark - Checkin Notification
  238. - (void)checkinFetched:(NSNotification *)notification {
  239. // A failed checkin may have been the reason for the connection failure. Attempt a connection
  240. // if the checkin fetched notification is fired.
  241. if (self.stayConnected && !self.isConnected) {
  242. [self connect];
  243. }
  244. }
  245. #pragma mark - Messages
  246. - (void)sendMessage:(GPBMessage *)message {
  247. [self.connection sendProto:message];
  248. }
  249. - (void)sendOnConnectOrDrop:(GPBMessage *)message {
  250. [self.connection sendOnConnectOrDrop:message];
  251. }
  252. #pragma mark - FIRMessagingConnectionDelegate
  253. - (void)connection:(FIRMessagingConnection *)fcmConnection
  254. didCloseForReason:(FIRMessagingConnectionCloseReason)reason {
  255. self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
  256. if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) {
  257. // Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in
  258. // failed, due to a connection error caused by bad network.
  259. [NSObject cancelPreviousPerformRequestsWithTarget:self
  260. selector:@selector(didConnectTimeout)
  261. object:nil];
  262. }
  263. if (self.stayConnected) {
  264. [self scheduleConnectRetry];
  265. }
  266. }
  267. - (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection {
  268. // Cancel the not-yet-triggered timeout task.
  269. [NSObject cancelPreviousPerformRequestsWithTarget:self
  270. selector:@selector(didConnectTimeout)
  271. object:nil];
  272. self.connectRetryCount = 0;
  273. self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
  274. [self.dataMessageManager setDeviceAuthID:[FIRInstanceID instanceID].deviceAuthID
  275. secretToken:[FIRInstanceID instanceID].secretToken];
  276. if (self.connectHandler) {
  277. self.connectHandler(nil);
  278. // notified the third party app with the registrationId.
  279. // we don't want them to know about the connection status and how it changes
  280. // so remove this handler
  281. self.connectHandler = nil;
  282. }
  283. }
  284. - (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message {
  285. NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
  286. if ([parsedMessage count]) {
  287. [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
  288. }
  289. }
  290. - (void)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds {
  291. NSSet *rmqIDSet = [NSSet setWithArray:rmqIds];
  292. NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count];
  293. [self.rmq2Manager scanWithRmqMessageHandler:^(NSDictionary *messages) {
  294. for (NSString *rmqID in messages) {
  295. GPBMessage *proto = messages[rmqID];
  296. GtalkDataMessageStanza *stanza = (GtalkDataMessageStanza *)proto;
  297. if ([rmqIDSet containsObject:rmqID]) {
  298. [messagesSent addObject:stanza];
  299. }
  300. }
  301. }];
  302. for (GtalkDataMessageStanza *message in messagesSent) {
  303. [self.dataMessageManager didSendDataMessageStanza:message];
  304. }
  305. [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds];
  306. }
  307. #pragma mark - Private
  308. - (void)setupConnectionAndConnect {
  309. [self setupConnection];
  310. [self tryToConnect];
  311. }
  312. - (void)setupConnection {
  313. NSString *host = FIRMessagingServerHost();
  314. NSUInteger port = FIRMessagingServerPort();
  315. if (self.connection != nil) {
  316. // if there is an old connection, explicitly sign it off.
  317. [self.connection signOut];
  318. self.connection.delegate = nil;
  319. }
  320. self.connection =
  321. [[FIRMessagingConnection alloc] initWithAuthID:[FIRInstanceID instanceID].deviceAuthID
  322. token:[FIRInstanceID instanceID].secretToken
  323. host:host
  324. port:port
  325. runLoop:[NSRunLoop mainRunLoop]
  326. rmq2Manager:self.rmq2Manager
  327. fcmManager:self.dataMessageManager];
  328. self.connection.delegate = self;
  329. }
  330. - (void)tryToConnect {
  331. if (!self.stayConnected) {
  332. return;
  333. }
  334. // Cancel any other pending signin requests.
  335. [NSObject cancelPreviousPerformRequestsWithTarget:self
  336. selector:@selector(tryToConnect)
  337. object:nil];
  338. NSString *deviceAuthID = [FIRInstanceID instanceID].deviceAuthID;
  339. NSString *secretToken = [FIRInstanceID instanceID].secretToken;
  340. if (deviceAuthID.length == 0 || secretToken.length == 0 || !self.connection) {
  341. FIRMessagingLoggerWarn(
  342. kFIRMessagingMessageCodeClientInvalidState,
  343. @"Invalid state to connect, deviceAuthID: %@, secretToken: %@, connection state: %ld",
  344. deviceAuthID, secretToken, (long)self.connection.state);
  345. return;
  346. }
  347. // Do not re-sign in if there is already a connection in progress.
  348. if (self.connection.state != kFIRMessagingConnectionNotConnected) {
  349. return;
  350. }
  351. self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1);
  352. [self performSelector:@selector(didConnectTimeout)
  353. withObject:nil
  354. afterDelay:self.connectionTimeoutInterval];
  355. [self.connection signIn];
  356. }
  357. - (void)didConnectTimeout {
  358. if (self.connection.state == kFIRMessagingConnectionSignedIn) {
  359. FIRMessagingLoggerWarn(kFIRMessagingMessageCodeClientInvalidStateTimeout,
  360. @"Invalid state for connection timeout.");
  361. }
  362. if (self.stayConnected) {
  363. [self.connection signOut];
  364. [self scheduleConnectRetry];
  365. }
  366. }
  367. #pragma mark - Schedulers
  368. - (void)scheduleConnectRetry {
  369. GULReachabilityStatus status = self.reachability.reachabilityStatus;
  370. BOOL isReachable = (status == kGULReachabilityViaWifi || status == kGULReachabilityViaCellular);
  371. if (!isReachable) {
  372. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010,
  373. @"Internet not reachable when signing into MCS during a retry");
  374. FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy];
  375. // disconnect before issuing a callback
  376. [self disconnectWithTryToConnectLater:YES];
  377. NSError *error =
  378. [NSError messagingErrorWithCode:kFIRMessagingErrorCodeNetwork
  379. failureReason:@"No internet available, cannot connect to FIRMessaging"];
  380. if (handler) {
  381. handler(error);
  382. self.connectHandler = nil;
  383. }
  384. return;
  385. }
  386. NSUInteger retryInterval = [self nextRetryInterval];
  387. FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011,
  388. @"Failed to sign in to MCS, retry in %lu seconds",
  389. _FIRMessaging_UL(retryInterval));
  390. [self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval];
  391. }
  392. - (NSUInteger)nextRetryInterval {
  393. return 1u << self.connectRetryCount;
  394. }
  395. @end