/* * 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 "FIRMessagingPendingTopicsList.h" #import "FIRMessaging_Private.h" #import "FIRMessagingLogger.h" #import "FIRMessagingPubSub.h" #import "FIRMessagingDefines.h" NSString *const kPendingTopicBatchActionKey = @"action"; NSString *const kPendingTopicBatchTopicsKey = @"topics"; NSString *const kPendingBatchesEncodingKey = @"batches"; NSString *const kPendingTopicsTimestampEncodingKey = @"ts"; #pragma mark - FIRMessagingTopicBatch @interface FIRMessagingTopicBatch () @property(nonatomic, strong, nonnull) NSMutableDictionary *> *topicHandlers; @end @implementation FIRMessagingTopicBatch - (instancetype)initWithAction:(FIRMessagingTopicAction)action { if (self = [super init]) { _action = action; _topics = [NSMutableSet set]; _topicHandlers = [NSMutableDictionary dictionary]; } return self; } #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey]; [aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { // Ensure that our integer -> enum casting is safe NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey]; FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe; if (actionRawValue == FIRMessagingTopicActionUnsubscribe) { action = FIRMessagingTopicActionUnsubscribe; } if (self = [self initWithAction:action]) { NSSet *topics = [aDecoder decodeObjectForKey:kPendingTopicBatchTopicsKey]; if ([topics isKindOfClass:[NSSet class]]) { _topics = [topics mutableCopy]; } _topicHandlers = [NSMutableDictionary dictionary]; } return self; } @end #pragma mark - FIRMessagingPendingTopicsList @interface FIRMessagingPendingTopicsList () @property(nonatomic, readwrite, strong) NSDate *archiveDate; @property(nonatomic, strong) NSMutableArray *topicBatches; @property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch; @property(nonatomic, strong) NSMutableSet *topicsInFlight; @end @implementation FIRMessagingPendingTopicsList - (instancetype)init { if (self = [super init]) { _topicBatches = [NSMutableArray array]; _topicsInFlight = [NSMutableSet set]; } return self; } + (void)pruneTopicBatches:(NSMutableArray *)topicBatches { // For now, just remove empty batches. In the future we can use this to make the subscriptions // more efficient, by actually pruning topic actions that cancel each other out, for example. for (NSInteger i = topicBatches.count-1; i >= 0; i--) { FIRMessagingTopicBatch *batch = topicBatches[i]; if (batch.topics.count == 0) { [topicBatches removeObjectAtIndex:i]; } } } #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey]; [aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey]; } - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [self init]) { _archiveDate = [aDecoder decodeObjectForKey:kPendingTopicsTimestampEncodingKey]; NSArray *archivedBatches = [aDecoder decodeObjectForKey:kPendingBatchesEncodingKey]; if (archivedBatches) { _topicBatches = [archivedBatches mutableCopy]; [FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches]; } _topicsInFlight = [NSMutableSet set]; } return self; } #pragma mark Getters - (NSUInteger)numberOfBatches { return self.topicBatches.count; } #pragma mark Adding/Removing topics - (void)addOperationForTopic:(NSString *)topic withAction:(FIRMessagingTopicAction)action completion:(nullable FIRMessagingTopicOperationCompletion)completion { FIRMessagingTopicBatch *lastBatch = nil; @synchronized (self) { lastBatch = self.topicBatches.lastObject; if (!lastBatch || lastBatch.action != action) { // There either was no last batch, or our last batch's action was not the same, so we have to // create a new batch lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action]; [self.topicBatches addObject:lastBatch]; } BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil); if (!topicExistedBefore) { [lastBatch.topics addObject:topic]; [self.delegate pendingTopicsListDidUpdate:self]; } // Add the completion handler to the batch if (completion) { NSMutableArray *handlers = lastBatch.topicHandlers[topic]; if (!handlers) { handlers = [[NSMutableArray alloc] init]; } [handlers addObject:completion]; lastBatch.topicHandlers[topic] = handlers; } if (!self.currentBatch) { self.currentBatch = lastBatch; } // This may have been the first topic added, or was added to an ongoing batch if (self.currentBatch == lastBatch && !topicExistedBefore) { // Add this topic to our ongoing operations FIRMessaging_WEAKIFY(self); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ FIRMessaging_STRONGIFY(self); [self resumeOperationsIfNeeded]; }); } } } - (void)resumeOperationsIfNeeded { @synchronized (self) { // If current batch is not set, set it now if (!self.currentBatch) { self.currentBatch = self.topicBatches.firstObject; } if (self.currentBatch.topics.count == 0) { return; } if (!self.delegate) { FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000, @"Attempted to update pending topics without a delegate"); return; } if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) { return; } for (NSString *topic in self.currentBatch.topics) { if ([self.topicsInFlight member:topic]) { // This topic is already active, so skip continue; } [self beginUpdateForCurrentBatchTopic:topic]; } } } - (BOOL)subscriptionErrorIsRecoverable:(NSError *)error { return [error.domain isEqualToString:NSURLErrorDomain]; } - (void)beginUpdateForCurrentBatchTopic:(NSString *)topic { @synchronized (self) { [self.topicsInFlight addObject:topic]; } FIRMessaging_WEAKIFY(self); [self.delegate pendingTopicsList:self requestedUpdateForTopic:topic action:self.currentBatch.action completion:^(NSError *error) { dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ FIRMessaging_STRONGIFY(self); @synchronized(self) { [self.topicsInFlight removeObject:topic]; BOOL recoverableError = [self subscriptionErrorIsRecoverable:error]; if (!error || !recoverableError) { // Notify our handlers and remove the topic from our batch NSMutableArray *handlers = self.currentBatch.topicHandlers[topic]; if (handlers.count) { dispatch_async(dispatch_get_main_queue(), ^{ for (FIRMessagingTopicOperationCompletion handler in handlers) { handler(error); } [handlers removeAllObjects]; }); } [self.currentBatch.topics removeObject:topic]; [self.currentBatch.topicHandlers removeObjectForKey:topic]; if (self.currentBatch.topics.count == 0) { // All topic updates successfully finished in this batch, move on // to the next batch [self.topicBatches removeObject:self.currentBatch]; self.currentBatch = nil; } [self.delegate pendingTopicsListDidUpdate:self]; FIRMessaging_WEAKIFY(self); dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ FIRMessaging_STRONGIFY(self); [self resumeOperationsIfNeeded]; }); } } }); }]; } @end