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.
506 lines
19 KiB
506 lines
19 KiB
/*
|
|
* 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 <FirebaseCore/FIRReachabilityChecker.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) FIRReachabilityChecker *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:(FIRReachabilityChecker *)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 {
|
|
FIRReachabilityStatus status = self.reachability.reachabilityStatus;
|
|
BOOL isReachable = (status == kFIRReachabilityViaWifi || status == kFIRReachabilityViaCellular);
|
|
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
|