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