FIRIAMMessageClientCache.m 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  17. #import "FIRCore+InAppMessaging.h"
  18. #import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h"
  19. #import "FIRIAMDisplayTriggerDefinition.h"
  20. #import "FIRIAMFetchResponseParser.h"
  21. #import "FIRIAMMessageClientCache.h"
  22. #import "FIRIAMServerMsgFetchStorage.h"
  23. @interface FIRIAMMessageClientCache ()
  24. // messages not for client-side testing
  25. @property(nonatomic) NSMutableArray<FIRIAMMessageDefinition *> *regularMessages;
  26. // messages for client-side testing
  27. @property(nonatomic) NSMutableArray<FIRIAMMessageDefinition *> *testMessages;
  28. @property(nonatomic, weak) id<FIRIAMCacheDataObserver> observer;
  29. @property(nonatomic) NSMutableSet<NSString *> *firebaseAnalyticEventsToWatch;
  30. @property(nonatomic) id<FIRIAMBookKeeper> bookKeeper;
  31. @property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser;
  32. @end
  33. // Methods doing read and write operations on messages field is synchronized to avoid
  34. // race conditions like change the array while iterating through it
  35. @implementation FIRIAMMessageClientCache
  36. - (instancetype)initWithBookkeeper:(id<FIRIAMBookKeeper>)bookKeeper
  37. usingResponseParser:(FIRIAMFetchResponseParser *)responseParser {
  38. if (self = [super init]) {
  39. _bookKeeper = bookKeeper;
  40. _responseParser = responseParser;
  41. }
  42. return self;
  43. }
  44. - (void)setDataObserver:(id<FIRIAMCacheDataObserver>)observer {
  45. self.observer = observer;
  46. }
  47. // reset messages data
  48. - (void)setMessageData:(NSArray<FIRIAMMessageDefinition *> *)messages {
  49. @synchronized(self) {
  50. NSSet<NSString *> *impressionSet =
  51. [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
  52. NSMutableArray<FIRIAMMessageDefinition *> *regularMessages = [[NSMutableArray alloc] init];
  53. self.testMessages = [[NSMutableArray alloc] init];
  54. // split between test vs non-test messages
  55. for (FIRIAMMessageDefinition *next in messages) {
  56. if (next.isTestMessage) {
  57. [self.testMessages addObject:next];
  58. } else {
  59. [regularMessages addObject:next];
  60. }
  61. }
  62. // while resetting the whole message set, we do prefiltering based on the impressions
  63. // data to get rid of messages we don't care so that the future searches are more efficient
  64. NSPredicate *notImpressedPredicate =
  65. [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
  66. FIRIAMMessageDefinition *message = (FIRIAMMessageDefinition *)evaluatedObject;
  67. return ![impressionSet containsObject:message.renderData.messageID];
  68. }];
  69. self.regularMessages =
  70. [[regularMessages filteredArrayUsingPredicate:notImpressedPredicate] mutableCopy];
  71. [self setupAnalyticsEventListening];
  72. }
  73. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160001",
  74. @"There are %lu test messages and %lu regular messages and "
  75. "%lu Firebase Analytics events to watch after "
  76. "resetting the message cache",
  77. (unsigned long)self.testMessages.count, (unsigned long)self.regularMessages.count,
  78. (unsigned long)self.firebaseAnalyticEventsToWatch.count);
  79. [self.observer dataChanged];
  80. }
  81. // triggered after self.messages are updated so that we can correctly enable/disable listening
  82. // on analytics event based on current fiam message set
  83. - (void)setupAnalyticsEventListening {
  84. self.firebaseAnalyticEventsToWatch = [[NSMutableSet alloc] init];
  85. for (FIRIAMMessageDefinition *nextMessage in self.regularMessages) {
  86. // if it's event based triggering, add it to the watch set
  87. for (FIRIAMDisplayTriggerDefinition *nextTrigger in nextMessage.renderTriggers) {
  88. if (nextTrigger.triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent) {
  89. [self.firebaseAnalyticEventsToWatch addObject:nextTrigger.firebaseEventName];
  90. }
  91. }
  92. }
  93. if (self.analycisEventDislayCheckFlow) {
  94. if ([self.firebaseAnalyticEventsToWatch count] > 0) {
  95. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160010",
  96. @"There are analytics event trigger based messages, enable listening");
  97. [self.analycisEventDislayCheckFlow start];
  98. } else {
  99. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160011",
  100. @"No analytics event trigger based messages, disable listening");
  101. [self.analycisEventDislayCheckFlow stop];
  102. }
  103. }
  104. }
  105. - (NSArray<FIRIAMMessageDefinition *> *)allRegularMessages {
  106. return [self.regularMessages copy];
  107. }
  108. - (BOOL)hasTestMessage {
  109. return self.testMessages.count > 0;
  110. }
  111. - (nullable FIRIAMMessageDefinition *)nextOnAppLaunchDisplayMsg {
  112. return [self nextMessageForTrigger:FIRIAMRenderTriggerOnAppLaunch];
  113. }
  114. - (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg {
  115. @synchronized(self) {
  116. // always first check test message which always have higher prirority
  117. if (self.testMessages.count > 0) {
  118. FIRIAMMessageDefinition *testMessage = self.testMessages[0];
  119. // always remove test message right away when being fetched for display
  120. [self.testMessages removeObjectAtIndex:0];
  121. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160007",
  122. @"Returning a test message for app foreground display");
  123. return testMessage;
  124. }
  125. }
  126. // otherwise check for a message from a published campaign
  127. return [self nextMessageForTrigger:FIRIAMRenderTriggerOnAppForeground];
  128. }
  129. - (nullable FIRIAMMessageDefinition *)nextMessageForTrigger:(FIRIAMRenderTrigger)trigger {
  130. // search from the start to end in the list (which implies the display priority) for the
  131. // first match (some messages in the cache may not be eligible for the current display
  132. // message fetch
  133. NSSet<NSString *> *impressionSet =
  134. [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
  135. @synchronized(self) {
  136. for (FIRIAMMessageDefinition *next in self.regularMessages) {
  137. // message being active and message not impressed yet
  138. if ([next messageHasStarted] && ![next messageHasExpired] &&
  139. ![impressionSet containsObject:next.renderData.messageID] &&
  140. [next messageRenderedOnTrigger:trigger]) {
  141. return next;
  142. }
  143. }
  144. }
  145. return nil;
  146. }
  147. - (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName {
  148. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160005",
  149. @"Inside nextOnFirebaseAnalyticEventDisplay for checking contextual trigger match");
  150. if (![self.firebaseAnalyticEventsToWatch containsObject:eventName]) {
  151. return nil;
  152. }
  153. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160006",
  154. @"There could be a potential message match for analytics event %@", eventName);
  155. NSSet<NSString *> *impressionSet =
  156. [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
  157. @synchronized(self) {
  158. for (FIRIAMMessageDefinition *next in self.regularMessages) {
  159. // message being active and message not impressed yet and the contextual trigger condition
  160. // match
  161. if ([next messageHasStarted] && ![next messageHasExpired] &&
  162. ![impressionSet containsObject:next.renderData.messageID] &&
  163. [next messageRenderedOnAnalyticsEvent:eventName]) {
  164. return next;
  165. }
  166. }
  167. }
  168. return nil;
  169. }
  170. - (void)removeMessageWithId:(NSString *)messageID {
  171. FIRIAMMessageDefinition *msgToRemove = nil;
  172. @synchronized(self) {
  173. for (FIRIAMMessageDefinition *next in self.regularMessages) {
  174. if ([next.renderData.messageID isEqualToString:messageID]) {
  175. msgToRemove = next;
  176. break;
  177. }
  178. }
  179. if (msgToRemove) {
  180. [self.regularMessages removeObject:msgToRemove];
  181. [self setupAnalyticsEventListening];
  182. }
  183. }
  184. // triggers the observer outside synchronization block
  185. if (msgToRemove) {
  186. [self.observer dataChanged];
  187. }
  188. }
  189. - (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage
  190. withCompletion:(void (^)(BOOL success))completion {
  191. [fetchStorage readResponseDictionary:^(NSDictionary *_Nonnull response, BOOL success) {
  192. if (success) {
  193. NSInteger discardCount;
  194. NSNumber *fetchWaitTime;
  195. NSArray<FIRIAMMessageDefinition *> *messagesFromStorage =
  196. [self.responseParser parseAPIResponseDictionary:response
  197. discardedMsgCount:&discardCount
  198. fetchWaitTimeInSeconds:&fetchWaitTime];
  199. [self setMessageData:messagesFromStorage];
  200. completion(YES);
  201. } else {
  202. completion(NO);
  203. }
  204. }];
  205. }
  206. @end