FIRIAMMessageClientCache.m 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 || TARGET_OS_VISION
  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.analyticsEventDisplayCheckFlow) {
  96. if ([self.firebaseAnalyticEventsToWatch count] > 0) {
  97. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160010",
  98. @"There are analytics event trigger based messages, enable listening");
  99. [self.analyticsEventDisplayCheckFlow start];
  100. } else {
  101. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160011",
  102. @"No analytics event trigger based messages, disable listening");
  103. [self.analyticsEventDisplayCheckFlow stop];
  104. }
  105. }
  106. }
  107. - (NSArray<FIRIAMMessageDefinition *> *)allRegularMessages {
  108. return [self.regularMessages copy];
  109. }
  110. - (BOOL)hasTestMessage {
  111. @synchronized(self) {
  112. return self.testMessages.count > 0;
  113. }
  114. }
  115. - (nullable FIRIAMMessageDefinition *)nextOnAppLaunchDisplayMsg {
  116. return [self nextMessageForTrigger:FIRIAMRenderTriggerOnAppLaunch];
  117. }
  118. - (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg {
  119. @synchronized(self) {
  120. // always first check test message which always have higher priority
  121. if (self.testMessages.count > 0) {
  122. FIRIAMMessageDefinition *testMessage = self.testMessages[0];
  123. // always remove test message right away when being fetched for display
  124. [self.testMessages removeObjectAtIndex:0];
  125. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160007",
  126. @"Returning a test message for app foreground display");
  127. return testMessage;
  128. }
  129. }
  130. // otherwise check for a message from a published campaign
  131. return [self nextMessageForTrigger:FIRIAMRenderTriggerOnAppForeground];
  132. }
  133. - (nullable FIRIAMMessageDefinition *)nextMessageForTrigger:(FIRIAMRenderTrigger)trigger {
  134. // search from the start to end in the list (which implies the display priority) for the
  135. // first match (some messages in the cache may not be eligible for the current display
  136. // message fetch
  137. NSSet<NSString *> *impressionSet =
  138. [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
  139. @synchronized(self) {
  140. for (FIRIAMMessageDefinition *next in self.regularMessages) {
  141. // message being active and message not impressed yet
  142. if ([next messageHasStarted] && ![next messageHasExpired] &&
  143. ![impressionSet containsObject:next.renderData.messageID] &&
  144. [next messageRenderedOnTrigger:trigger]) {
  145. return next;
  146. }
  147. }
  148. }
  149. return nil;
  150. }
  151. - (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName {
  152. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160005",
  153. @"Inside nextOnFirebaseAnalyticEventDisplay for checking contextual trigger match");
  154. if (![self.firebaseAnalyticEventsToWatch containsObject:eventName]) {
  155. return nil;
  156. }
  157. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160006",
  158. @"There could be a potential message match for analytics event %@", eventName);
  159. NSSet<NSString *> *impressionSet =
  160. [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
  161. @synchronized(self) {
  162. for (FIRIAMMessageDefinition *next in self.regularMessages) {
  163. // message being active and message not impressed yet and the contextual trigger condition
  164. // match
  165. if ([next messageHasStarted] && ![next messageHasExpired] &&
  166. ![impressionSet containsObject:next.renderData.messageID] &&
  167. [next messageRenderedOnAnalyticsEvent:eventName]) {
  168. return next;
  169. }
  170. }
  171. }
  172. return nil;
  173. }
  174. - (void)removeMessageWithId:(NSString *)messageID {
  175. FIRIAMMessageDefinition *msgToRemove = nil;
  176. @synchronized(self) {
  177. for (FIRIAMMessageDefinition *next in self.regularMessages) {
  178. if ([next.renderData.messageID isEqualToString:messageID]) {
  179. msgToRemove = next;
  180. break;
  181. }
  182. }
  183. if (msgToRemove) {
  184. [self.regularMessages removeObject:msgToRemove];
  185. [self setupAnalyticsEventListening];
  186. }
  187. }
  188. // triggers the observer outside synchronization block
  189. if (msgToRemove) {
  190. [self.observer messageDataChanged];
  191. }
  192. }
  193. - (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage
  194. withCompletion:(void (^)(BOOL success))completion {
  195. [fetchStorage readResponseDictionary:^(NSDictionary *_Nonnull response, BOOL success) {
  196. if (success) {
  197. NSInteger discardCount;
  198. NSNumber *fetchWaitTime;
  199. NSArray<FIRIAMMessageDefinition *> *messagesFromStorage =
  200. [self.responseParser parseAPIResponseDictionary:response
  201. discardedMsgCount:&discardCount
  202. fetchWaitTimeInSeconds:&fetchWaitTime];
  203. [self setMessageData:messagesFromStorage];
  204. completion(YES);
  205. } else {
  206. completion(NO);
  207. }
  208. }];
  209. }
  210. @end
  211. #endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION