added iOS source code
[wl-app.git] / iOS / Pods / FirebaseMessaging / Firebase / Messaging / FIRMessagingClient.m
diff --git a/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingClient.m b/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingClient.m
new file mode 100644 (file)
index 0000000..9d8c558
--- /dev/null
@@ -0,0 +1,506 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRMessagingClient.h"
+
+#import <GoogleUtilities/GULReachabilityChecker.h>
+
+#import "FIRMessaging.h"
+#import "FIRMessagingConnection.h"
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDataMessageManager.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingRegistrar.h"
+#import "FIRMessagingRmqManager.h"
+#import "FIRMessagingTopicsCommon.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+
+static const NSTimeInterval kConnectTimeoutInterval = 40.0;
+static const NSTimeInterval kReconnectDelayInSeconds = 2 * 60; // 2 minutes
+
+static const NSUInteger kMaxRetryExponent = 10;  // 2^10 = 1024 seconds ~= 17 minutes
+
+static NSString *const kFIRMessagingMCSServerHost = @"mtalk.google.com";
+static NSUInteger const kFIRMessagingMCSServerPort = 5228;
+
+// register device with checkin
+typedef void(^FIRMessagingRegisterDeviceHandler)(NSError *error);
+
+static NSString *FIRMessagingServerHost() {
+  static NSString *serverHost = nil;
+  static dispatch_once_t onceToken;
+  dispatch_once(&onceToken, ^{
+    NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+    NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
+    NSString *host = [customServerHostAndPort componentsSeparatedByString:@":"].firstObject;
+    if (host) {
+      serverHost = host;
+    } else {
+      serverHost = kFIRMessagingMCSServerHost;
+    }
+  });
+  return serverHost;
+}
+
+static NSUInteger FIRMessagingServerPort() {
+  static NSUInteger serverPort = kFIRMessagingMCSServerPort;
+  static dispatch_once_t onceToken;
+  dispatch_once(&onceToken, ^{
+    NSDictionary *environment = [[NSProcessInfo processInfo] environment];
+    NSString *customServerHostAndPort = environment[@"FCM_MCS_HOST"];
+    NSArray<NSString *> *components = [customServerHostAndPort componentsSeparatedByString:@":"];
+    NSUInteger port = (NSUInteger)[components.lastObject integerValue];
+    if (port != 0) {
+      serverPort = port;
+    }
+  });
+  return serverPort;
+}
+
+@interface FIRMessagingClient () <FIRMessagingConnectionDelegate>
+
+@property(nonatomic, readwrite, weak) id<FIRMessagingClientDelegate> clientDelegate;
+@property(nonatomic, readwrite, strong) FIRMessagingConnection *connection;
+@property(nonatomic, readwrite, strong) FIRMessagingRegistrar *registrar;
+
+@property(nonatomic, readwrite, strong) NSString *senderId;
+
+// FIRMessagingService owns these instances
+@property(nonatomic, readwrite, weak) FIRMessagingRmqManager *rmq2Manager;
+@property(nonatomic, readwrite, weak) GULReachabilityChecker *reachability;
+
+@property(nonatomic, readwrite, assign) int64_t lastConnectedTimestamp;
+@property(nonatomic, readwrite, assign) int64_t lastDisconnectedTimestamp;
+@property(nonatomic, readwrite, assign) NSUInteger connectRetryCount;
+
+// Should we stay connected to MCS or not. Should be YES throughout the lifetime
+// of a MCS connection. If set to NO it signifies that an existing MCS connection
+// should be disconnected.
+@property(nonatomic, readwrite, assign) BOOL stayConnected;
+@property(nonatomic, readwrite, assign) NSTimeInterval connectionTimeoutInterval;
+
+// Used if the MCS connection suddenly breaksdown in the middle and we want to reconnect
+// with some permissible delay we schedule a reconnect and set it to YES and when it's
+// scheduled this will be set back to NO.
+@property(nonatomic, readwrite, assign) BOOL didScheduleReconnect;
+
+// handlers
+@property(nonatomic, readwrite, copy) FIRMessagingConnectCompletionHandler connectHandler;
+
+@end
+
+@implementation FIRMessagingClient
+
+- (instancetype)init {
+  FIRMessagingInvalidateInitializer();
+}
+
+- (instancetype)initWithDelegate:(id<FIRMessagingClientDelegate>)delegate
+                    reachability:(GULReachabilityChecker *)reachability
+                     rmq2Manager:(FIRMessagingRmqManager *)rmq2Manager {
+  self = [super init];
+  if (self) {
+    _reachability = reachability;
+    _clientDelegate = delegate;
+    _rmq2Manager = rmq2Manager;
+    _registrar = [[FIRMessagingRegistrar alloc] init];
+    _connectionTimeoutInterval = kConnectTimeoutInterval;
+    // Listen for checkin fetch notifications, as connecting to MCS may have failed due to
+    // missing checkin info (while it was being fetched).
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(checkinFetched:)
+                                                 name:kFIRMessagingCheckinFetchedNotification
+                                               object:nil];
+  }
+  return self;
+}
+
+- (void)teardown {
+  FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient000, @"");
+  self.stayConnected = NO;
+
+  // Clear all the handlers
+  self.connectHandler = nil;
+
+  [self.connection teardown];
+
+  // Stop all subscription requests
+  [self.registrar cancelAllRequests];
+
+  _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected, @"Did not disconnect");
+  [NSObject cancelPreviousPerformRequestsWithTarget:self];
+
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)cancelAllRequests {
+  // Stop any checkin requests or any subscription requests
+  [self.registrar cancelAllRequests];
+
+  // Stop any future connection requests to MCS
+  if (self.stayConnected && self.isConnected && !self.isConnectionActive) {
+    self.stayConnected = NO;
+    [NSObject cancelPreviousPerformRequestsWithTarget:self];
+  }
+}
+
+#pragma mark - FIRMessaging subscribe
+
+- (void)updateSubscriptionWithToken:(NSString *)token
+                              topic:(NSString *)topic
+                            options:(NSDictionary *)options
+                       shouldDelete:(BOOL)shouldDelete
+                            handler:(FIRMessagingTopicOperationCompletion)handler {
+
+  _FIRMessagingDevAssert(handler != nil, @"Invalid handler to FIRMessaging subscribe");
+
+  FIRMessagingTopicOperationCompletion completion = ^void(NSError *error) {
+    if (error) {
+      FIRMessagingLoggerError(kFIRMessagingMessageCodeClient001, @"Failed to subscribe to topic %@",
+                              error);
+    } else {
+      if (shouldDelete) {
+        FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient002,
+                               @"Successfully unsubscribed from topic %@", topic);
+      } else {
+        FIRMessagingLoggerInfo(kFIRMessagingMessageCodeClient003,
+                               @"Successfully subscribed to topic %@", topic);
+      }
+    }
+    handler(error);
+  };
+
+  [self.registrar updateSubscriptionToTopic:topic
+                                  withToken:token
+                                    options:options
+                               shouldDelete:shouldDelete
+                                    handler:completion];
+}
+
+#pragma mark - MCS Connection
+
+- (BOOL)isConnected {
+  return self.stayConnected && self.connection.state != kFIRMessagingConnectionNotConnected;
+}
+
+- (BOOL)isConnectionActive {
+  return self.stayConnected && self.connection.state == kFIRMessagingConnectionSignedIn;
+}
+
+- (BOOL)shouldStayConnected {
+  return self.stayConnected;
+}
+
+- (void)retryConnectionImmediately:(BOOL)immediately {
+  // Do not connect to an invalid host or an invalid port
+  if (!self.stayConnected || !self.connection.host || self.connection.port == 0) {
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient004,
+                            @"FIRMessaging connection will not reconnect to MCS. "
+                            @"Stay connected: %d",
+                            self.stayConnected);
+    return;
+  }
+  if (self.isConnectionActive) {
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient005,
+                            @"FIRMessaging Connection skip retry, active");
+    // already connected and logged in.
+    // Heartbeat alarm is set and will force close the connection
+    return;
+  }
+  if (self.isConnected) {
+    // already connected and logged in.
+    // Heartbeat alarm is set and will force close the connection
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient006,
+                            @"FIRMessaging Connection skip retry, connected");
+    return;
+  }
+
+  if (immediately) {
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient007,
+                            @"Try to connect to MCS immediately");
+    [self tryToConnect];
+  } else {
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient008, @"Try to connect to MCS lazily");
+    // Avoid all the other logic that we have in other clients, since this would always happen
+    // when the app is in the foreground and since the FIRMessaging connection isn't shared with any other
+    // app we can be more aggressive in reconnections
+    if (!self.didScheduleReconnect) {
+      FIRMessaging_WEAKIFY(self);
+      dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+                                   (int64_t)(kReconnectDelayInSeconds * NSEC_PER_SEC)),
+                     dispatch_get_main_queue(), ^{
+                         FIRMessaging_STRONGIFY(self);
+                         self.didScheduleReconnect = NO;
+                         [self tryToConnect];
+                     });
+
+      self.didScheduleReconnect = YES;
+    }
+  }
+}
+
+- (void)connectWithHandler:(FIRMessagingConnectCompletionHandler)handler {
+  if (self.isConnected) {
+    NSError *error = [NSError fcm_errorWithCode:kFIRMessagingErrorCodeAlreadyConnected
+                                       userInfo:@{
+        NSLocalizedFailureReasonErrorKey: @"FIRMessaging is already connected",
+        }];
+    handler(error);
+    return;
+  }
+  self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+  self.connectHandler = handler;
+  [self connect];
+}
+
+- (void)connect {
+  // reset retry counts
+  self.connectRetryCount = 0;
+
+  if (self.isConnected) {
+    return;
+  }
+
+  self.stayConnected = YES;
+  if (![self.registrar tryToLoadValidCheckinInfo]) {
+    // Checkin info is not available. This may be due to the checkin still being fetched.
+    if (self.connectHandler) {
+      NSError *error = [NSError errorWithFCMErrorCode:kFIRMessagingErrorCodeMissingDeviceID];
+      self.connectHandler(error);
+    }
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient009,
+                            @"Failed to connect to MCS. No deviceID and secret found.");
+    // Return for now. If checkin is, in fact, retrieved, the
+    // |kFIRMessagingCheckinFetchedNotification| will be fired.
+    return;
+  }
+  [self setupConnectionAndConnect];
+}
+
+- (void)disconnect {
+  // user called disconnect
+  // We don't want to connect later even if no network is available.
+  [self disconnectWithTryToConnectLater:NO];
+}
+
+/**
+ *  Disconnect the current client connection. Also explicitly stop and connction retries.
+ *
+ *  @param tryToConnectLater If YES will try to connect later when sending upstream messages
+ *                           else if NO do not connect again until user explicitly calls
+ *                           connect.
+ */
+- (void)disconnectWithTryToConnectLater:(BOOL)tryToConnectLater {
+
+  self.stayConnected = tryToConnectLater;
+  [self.connection signOut];
+  _FIRMessagingDevAssert(self.connection.state == kFIRMessagingConnectionNotConnected,
+                @"FIRMessaging connection did not disconnect");
+
+  // since we can disconnect while still trying to establish the connection it's required to
+  // cancel all performSelectors else the object might be retained
+  [NSObject cancelPreviousPerformRequestsWithTarget:self
+                                           selector:@selector(tryToConnect)
+                                             object:nil];
+  [NSObject cancelPreviousPerformRequestsWithTarget:self
+                                           selector:@selector(didConnectTimeout)
+                                             object:nil];
+  self.connectHandler = nil;
+}
+
+#pragma mark - Checkin Notification
+- (void)checkinFetched:(NSNotification *)notification {
+  // A failed checkin may have been the reason for the connection failure. Attempt a connection
+  // if the checkin fetched notification is fired.
+  if (self.stayConnected && !self.isConnected) {
+    [self connect];
+  }
+}
+
+#pragma mark - Messages
+
+- (void)sendMessage:(GPBMessage *)message {
+  [self.connection sendProto:message];
+}
+
+- (void)sendOnConnectOrDrop:(GPBMessage *)message {
+  [self.connection sendOnConnectOrDrop:message];
+}
+
+#pragma mark - FIRMessagingConnectionDelegate
+
+- (void)connection:(FIRMessagingConnection *)fcmConnection
+    didCloseForReason:(FIRMessagingConnectionCloseReason)reason {
+
+  self.lastDisconnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+
+  if (reason == kFIRMessagingConnectionCloseReasonSocketDisconnected) {
+    // Cancel the not-yet-triggered timeout task before rescheduling, in case the previous sign in
+    // failed, due to a connection error caused by bad network.
+    [NSObject cancelPreviousPerformRequestsWithTarget:self
+                                             selector:@selector(didConnectTimeout)
+                                               object:nil];
+  }
+  if (self.stayConnected) {
+    [self scheduleConnectRetry];
+  }
+}
+
+- (void)didLoginWithConnection:(FIRMessagingConnection *)fcmConnection {
+  // Cancel the not-yet-triggered timeout task.
+  [NSObject cancelPreviousPerformRequestsWithTarget:self
+                                           selector:@selector(didConnectTimeout)
+                                             object:nil];
+  self.connectRetryCount = 0;
+  self.lastConnectedTimestamp = FIRMessagingCurrentTimestampInMilliseconds();
+
+
+  [self.dataMessageManager setDeviceAuthID:self.registrar.deviceAuthID
+                               secretToken:self.registrar.secretToken];
+  if (self.connectHandler) {
+    self.connectHandler(nil);
+    // notified the third party app with the registrationId.
+    // we don't want them to know about the connection status and how it changes
+    // so remove this handler
+    self.connectHandler = nil;
+  }
+}
+
+- (void)connectionDidRecieveMessage:(GtalkDataMessageStanza *)message {
+  NSDictionary *parsedMessage = [self.dataMessageManager processPacket:message];
+  if ([parsedMessage count]) {
+    [self.dataMessageManager didReceiveParsedMessage:parsedMessage];
+  }
+}
+
+- (int)connectionDidReceiveAckForRmqIds:(NSArray *)rmqIds {
+  NSSet *rmqIDSet = [NSSet setWithArray:rmqIds];
+  NSMutableArray *messagesSent = [NSMutableArray arrayWithCapacity:rmqIds.count];
+  [self.rmq2Manager scanWithRmqMessageHandler:nil
+                           dataMessageHandler:^(int64_t rmqId, GtalkDataMessageStanza *stanza) {
+                             NSString *rmqIdString = [NSString stringWithFormat:@"%lld", rmqId];
+                             if ([rmqIDSet containsObject:rmqIdString]) {
+                               [messagesSent addObject:stanza];
+                             }
+                           }];
+  for (GtalkDataMessageStanza *message in messagesSent) {
+    [self.dataMessageManager didSendDataMessageStanza:message];
+  }
+  return [self.rmq2Manager removeRmqMessagesWithRmqIds:rmqIds];
+}
+
+#pragma mark - Private
+
+- (void)setupConnectionAndConnect {
+  [self setupConnection];
+  [self tryToConnect];
+}
+
+- (void)setupConnection {
+  NSString *host = FIRMessagingServerHost();
+  NSUInteger port = FIRMessagingServerPort();
+  _FIRMessagingDevAssert([host length] > 0 && port != 0, @"Invalid port or host");
+
+  if (self.connection != nil) {
+    // if there is an old connection, explicitly sign it off.
+    [self.connection signOut];
+    self.connection.delegate = nil;
+  }
+  self.connection = [[FIRMessagingConnection alloc] initWithAuthID:self.registrar.deviceAuthID
+                                                    token:self.registrar.secretToken
+                                                     host:host
+                                                     port:port
+                                                  runLoop:[NSRunLoop mainRunLoop]
+                                              rmq2Manager:self.rmq2Manager
+                                               fcmManager:self.dataMessageManager];
+  self.connection.delegate = self;
+}
+
+- (void)tryToConnect {
+  if (!self.stayConnected) {
+    return;
+  }
+
+  // Cancel any other pending signin requests.
+  [NSObject cancelPreviousPerformRequestsWithTarget:self
+                                           selector:@selector(tryToConnect)
+                                             object:nil];
+
+  // Do not re-sign in if there is already a connection in progress.
+  if (self.connection.state != kFIRMessagingConnectionNotConnected) {
+    return;
+  }
+
+  _FIRMessagingDevAssert(self.registrar.deviceAuthID.length > 0 &&
+                 self.registrar.secretToken.length > 0 &&
+                 self.connection != nil,
+                 @"Invalid state cannot connect");
+
+  self.connectRetryCount = MIN(kMaxRetryExponent, self.connectRetryCount + 1);
+  [self performSelector:@selector(didConnectTimeout)
+             withObject:nil
+             afterDelay:self.connectionTimeoutInterval];
+  [self.connection signIn];
+}
+
+- (void)didConnectTimeout {
+  _FIRMessagingDevAssert(self.connection.state != kFIRMessagingConnectionSignedIn,
+                @"Invalid state for MCS connection");
+
+  if (self.stayConnected) {
+    [self.connection signOut];
+    [self scheduleConnectRetry];
+  }
+}
+
+#pragma mark - Schedulers
+
+- (void)scheduleConnectRetry {
+  GULReachabilityStatus status = self.reachability.reachabilityStatus;
+  BOOL isReachable = (status == kGULReachabilityViaWifi || status == kGULReachabilityViaCellular);
+  if (!isReachable) {
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient010,
+                            @"Internet not reachable when signing into MCS during a retry");
+
+    FIRMessagingConnectCompletionHandler handler = [self.connectHandler copy];
+    // disconnect before issuing a callback
+    [self disconnectWithTryToConnectLater:YES];
+    NSError *error =
+        [NSError errorWithDomain:@"No internet available, cannot connect to FIRMessaging"
+                            code:kFIRMessagingErrorCodeNetwork
+                        userInfo:nil];
+    if (handler) {
+      handler(error);
+      self.connectHandler = nil;
+    }
+    return;
+  }
+
+  NSUInteger retryInterval = [self nextRetryInterval];
+
+  FIRMessagingLoggerDebug(kFIRMessagingMessageCodeClient011,
+                          @"Failed to sign in to MCS, retry in %lu seconds",
+                          _FIRMessaging_UL(retryInterval));
+  [self performSelector:@selector(tryToConnect) withObject:nil afterDelay:retryInterval];
+}
+
+- (NSUInteger)nextRetryInterval {
+  return 1u << self.connectRetryCount;
+}
+
+@end