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.

516 lines
19 KiB

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