added iOS source code
[wl-app.git] / iOS / Pods / FirebaseMessaging / Firebase / Messaging / FIRMessagingPendingTopicsList.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 "FIRMessagingPendingTopicsList.h"
18
19 #import "FIRMessaging_Private.h"
20 #import "FIRMessagingLogger.h"
21 #import "FIRMessagingPubSub.h"
22
23 #import "FIRMessagingDefines.h"
24
25 NSString *const kPendingTopicBatchActionKey = @"action";
26 NSString *const kPendingTopicBatchTopicsKey = @"topics";
27
28 NSString *const kPendingBatchesEncodingKey = @"batches";
29 NSString *const kPendingTopicsTimestampEncodingKey = @"ts";
30
31 #pragma mark - FIRMessagingTopicBatch
32
33 @interface FIRMessagingTopicBatch ()
34
35 @property(nonatomic, strong, nonnull) NSMutableDictionary
36     <NSString *, NSMutableArray <FIRMessagingTopicOperationCompletion> *> *topicHandlers;
37
38 @end
39
40 @implementation FIRMessagingTopicBatch
41
42 - (instancetype)initWithAction:(FIRMessagingTopicAction)action {
43   if (self = [super init]) {
44     _action = action;
45     _topics = [NSMutableSet set];
46     _topicHandlers = [NSMutableDictionary dictionary];
47   }
48   return self;
49 }
50
51 #pragma mark NSCoding
52
53 - (void)encodeWithCoder:(NSCoder *)aCoder {
54   [aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey];
55   [aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey];
56 }
57
58 - (instancetype)initWithCoder:(NSCoder *)aDecoder {
59
60   // Ensure that our integer -> enum casting is safe
61   NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey];
62   FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
63   if (actionRawValue == FIRMessagingTopicActionUnsubscribe) {
64     action = FIRMessagingTopicActionUnsubscribe;
65   }
66
67   if (self = [self initWithAction:action]) {
68     NSSet *topics = [aDecoder decodeObjectForKey:kPendingTopicBatchTopicsKey];
69     if ([topics isKindOfClass:[NSSet class]]) {
70       _topics = [topics mutableCopy];
71     }
72     _topicHandlers = [NSMutableDictionary dictionary];
73   }
74   return self;
75 }
76
77 @end
78
79 #pragma mark - FIRMessagingPendingTopicsList
80
81 @interface FIRMessagingPendingTopicsList ()
82
83 @property(nonatomic, readwrite, strong) NSDate *archiveDate;
84 @property(nonatomic, strong) NSMutableArray <FIRMessagingTopicBatch *> *topicBatches;
85
86 @property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch;
87 @property(nonatomic, strong) NSMutableSet <NSString *> *topicsInFlight;
88
89 @end
90
91 @implementation FIRMessagingPendingTopicsList
92
93 - (instancetype)init {
94   if (self = [super init]) {
95     _topicBatches = [NSMutableArray array];
96     _topicsInFlight = [NSMutableSet set];
97   }
98   return self;
99 }
100
101 + (void)pruneTopicBatches:(NSMutableArray <FIRMessagingTopicBatch *> *)topicBatches {
102   // For now, just remove empty batches. In the future we can use this to make the subscriptions
103   // more efficient, by actually pruning topic actions that cancel each other out, for example.
104   for (NSInteger i = topicBatches.count-1; i >= 0; i--) {
105     FIRMessagingTopicBatch *batch = topicBatches[i];
106     if (batch.topics.count == 0) {
107       [topicBatches removeObjectAtIndex:i];
108     }
109   }
110 }
111
112 #pragma mark NSCoding
113
114 - (void)encodeWithCoder:(NSCoder *)aCoder {
115   [aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey];
116   [aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey];
117 }
118
119 - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
120
121   if (self = [self init]) {
122     _archiveDate = [aDecoder decodeObjectForKey:kPendingTopicsTimestampEncodingKey];
123     NSArray *archivedBatches = [aDecoder decodeObjectForKey:kPendingBatchesEncodingKey];
124     if (archivedBatches) {
125       _topicBatches = [archivedBatches mutableCopy];
126       [FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches];
127     }
128     _topicsInFlight = [NSMutableSet set];
129   }
130   return self;
131 }
132
133 #pragma mark Getters
134
135 - (NSUInteger)numberOfBatches {
136   return self.topicBatches.count;
137 }
138
139 #pragma mark Adding/Removing topics
140
141 - (void)addOperationForTopic:(NSString *)topic
142                   withAction:(FIRMessagingTopicAction)action
143                   completion:(nullable FIRMessagingTopicOperationCompletion)completion {
144
145   FIRMessagingTopicBatch *lastBatch = nil;
146   @synchronized (self) {
147     lastBatch = self.topicBatches.lastObject;
148     if (!lastBatch || lastBatch.action != action) {
149       // There either was no last batch, or our last batch's action was not the same, so we have to
150       // create a new batch
151       lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action];
152       [self.topicBatches addObject:lastBatch];
153     }
154     BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil);
155     if (!topicExistedBefore) {
156       [lastBatch.topics addObject:topic];
157       [self.delegate pendingTopicsListDidUpdate:self];
158     }
159     // Add the completion handler to the batch
160     if (completion) {
161       NSMutableArray *handlers = lastBatch.topicHandlers[topic];
162       if (!handlers) {
163         handlers = [[NSMutableArray alloc] init];
164       }
165       [handlers addObject:completion];
166       lastBatch.topicHandlers[topic] = handlers;
167     }
168     if (!self.currentBatch) {
169       self.currentBatch = lastBatch;
170     }
171     // This may have been the first topic added, or was added to an ongoing batch
172     if (self.currentBatch == lastBatch && !topicExistedBefore) {
173       // Add this topic to our ongoing operations
174       FIRMessaging_WEAKIFY(self);
175       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
176         FIRMessaging_STRONGIFY(self);
177         [self resumeOperationsIfNeeded];
178       });
179     }
180   }
181 }
182
183 - (void)resumeOperationsIfNeeded {
184   @synchronized (self) {
185     // If current batch is not set, set it now
186     if (!self.currentBatch) {
187       self.currentBatch = self.topicBatches.firstObject;
188     }
189     if (self.currentBatch.topics.count == 0) {
190       return;
191     }
192     if (!self.delegate) {
193       FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000,
194                               @"Attempted to update pending topics without a delegate");
195       return;
196     }
197     if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) {
198       return;
199     }
200     for (NSString *topic in self.currentBatch.topics) {
201       if ([self.topicsInFlight member:topic]) {
202         // This topic is already active, so skip
203         continue;
204       }
205       [self beginUpdateForCurrentBatchTopic:topic];
206     }
207   }
208 }
209
210 - (BOOL)subscriptionErrorIsRecoverable:(NSError *)error {
211   return [error.domain isEqualToString:NSURLErrorDomain];
212 }
213
214 - (void)beginUpdateForCurrentBatchTopic:(NSString *)topic {
215
216   @synchronized (self) {
217     [self.topicsInFlight addObject:topic];
218   }
219   FIRMessaging_WEAKIFY(self);
220   [self.delegate
221             pendingTopicsList:self
222       requestedUpdateForTopic:topic
223                        action:self.currentBatch.action
224                    completion:^(NSError *error) {
225                      dispatch_async(
226                          dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
227                            FIRMessaging_STRONGIFY(self);
228                            @synchronized(self) {
229                              [self.topicsInFlight removeObject:topic];
230
231                              BOOL recoverableError = [self subscriptionErrorIsRecoverable:error];
232                              if (!error || !recoverableError) {
233                                // Notify our handlers and remove the topic from our batch
234                                NSMutableArray *handlers = self.currentBatch.topicHandlers[topic];
235                                if (handlers.count) {
236                                  dispatch_async(dispatch_get_main_queue(), ^{
237                                    for (FIRMessagingTopicOperationCompletion handler in handlers) {
238                                      handler(error);
239                                    }
240                                    [handlers removeAllObjects];
241                                  });
242                                }
243                                [self.currentBatch.topics removeObject:topic];
244                                [self.currentBatch.topicHandlers removeObjectForKey:topic];
245                                if (self.currentBatch.topics.count == 0) {
246                                  // All topic updates successfully finished in this batch, move on
247                                  // to the next batch
248                                  [self.topicBatches removeObject:self.currentBatch];
249                                  self.currentBatch = nil;
250                                }
251                                [self.delegate pendingTopicsListDidUpdate:self];
252                                FIRMessaging_WEAKIFY(self);
253                                dispatch_async(
254                                    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
255                                    ^{
256                                      FIRMessaging_STRONGIFY(self);
257                                      [self resumeOperationsIfNeeded];
258                                    });
259                              }
260                            }
261                          });
262                    }];
263 }
264
265 @end