FIRIAMMessageClientCache.m 9.3 KB

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